C++20s concepts with a forward declared type

In today's post, I would like to continue talking about forward declared or better incomplete types in C++.

I discussed some scenarios in last months post Forward declaring a type in C++: The good, and the bad. Today I like to add another flavor, C++20s concepts.

It depends

It's time to tune up the heat. Let's use C++20's concepts and play with the cat from last month:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
struct Cat;  A 

B 
template<typename T>
concept CanPlay = requires(const T val) { val.Play(); };

template<typename T>
void Play(const T& val)
{
  if constexpr(CanPlay<T>) {  C 
    puts("Playing with pets is great!");
  } else {
    puts("Sadly no pet to play with!");
  }
}

D 
void Fun(const Cat& c)
{
  Play(c);
}

struct Cat {
  E 
  void Play() const {}
};

void Use()
{
  F 
  Fun(Cat{});
}

This example starts as the initial one in this article by forward declaring the class Cat. Using C++20, I create a concept CanPlay in B checking that a type T has a member function Play. I use that concept inside Play in C. That way, Play prints Playing with pets is great! if the member function exists and otherwise Sadly, no pet to play with!.

The function Play is then used inside Fun, which itself takes Cat reference D.

Only after that, I add the definition of Cat, of course, with the member function Play E. Finally, in F, I create a Cat object and call Fun. All clear? Good!

Same question as in my last post: what do you think is the output of this program? The answer is just one down-scroll away.

Obviously:

1
Playing with pets is great!

That was an easy one, right? Sorry, I didn't want to disappoint you. How about now:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
struct Cat;  A 

template<typename T>  B 
concept CanPlay = requires(const T val) { val.Play(); };

template<typename T>
auto Play(const T& val)
{
  if constexpr(CanPlay<T>) {  C 
    puts("Playing with pets is great!");
  } else {
    puts("Sadly no pet to play with!");
  }
}

void Fun(const Cat& c)  D 
{
  Play(c);
}

struct Cat {
  void Play() const {}  E 
};

void Use()
{
  Fun(Cat{});  F 
}

You're looking for the change? Sorry, I made Play return auto. For sure nothing to worry about.

Programming with C++20

Get a copy of Programming with C++20

Want to get a better understanding of C++20? Check out my book which explains the new constructs Concepts, Coroutines, Ranges, Modules and more in detail.

The output now? Well, please take your time before scrolling down to see the answer.

1
Sadly no pet to play with!

Wow, why did the output change? Because of the auto return type. This forces the compiler to eagerly instantiate Play by looking into the function's body for the return type. Without auto the compilers delay the instantiation to the end of the translation unit. This isn't possible with auto return type. But the eager instantiation in Fun happens now at a time when Cat is still an incomplete type. That is no issue for the concept, it can work with incomplete types. However, the concept only has the knowledge of Cat present at this time. And that is, there is a type Cat without a member function Play. My post Efficient C++: The hidden compile-time cost of auto return types covers the implications a bit more.

There are other scenarios where the result of your concept can change when being used with an incomplete type.

Summary

I can only repeat last months summary: Using incomplete types to forward declare classes is a great way to reduce compile times. However, it also opens the door for undefined or at least surprising behavior. Be extra careful when forward-declaring types.

Andreas

Recent posts