1

I'd like a short clarification on how complete types relate to CRTP. I thought this question was somewhat related. However, my question here question pertains to CRTP where a derived class member function explicitly calls the base class member function, which in turn calls a derived function. This appears different from calling a base class function on a derived type once within the main routine.

I have also read this question where it was explained that the static constexpr members of the base class that make use of the derived class are not initialized until the derived class is seen by the compiler and is complete. However in that case, the derived class was also templated.

Here is my question. Consider

template<typename D> struct B{
  void foo() const { static_cast<const D*>(this)->baz(); }
};

struct D : B<D> {
  void bar() const { foo(); }
  void baz() const {}
};

As I understand, each D remains an incomplete type until the closing brace of the class. I also understand that member function templates are not instantiated until they are used. Thus, if we ignore D::bar for now, it makes sense that the following is valid in main:

D d; d.foo(); 

What I need further clarification about is what constitutes use? For example, in defining D::bar, there is a call to B::foo (so that function is presumably instantiated there) which would then require D to be a complete type. But at the point where D::bar is being defined, D is not complete. Or is it?

I thought perhaps what could be happening is that where D::bar is being defined and calls B::foo, it forces the compiler to make a declaration for B::foo (no definition required). And perhaps the definition does not happen until D::bar is actually called at some other point. But I ran this at C++ Insights and became more confused as the explicit specialization of B to B<D> happens even before D is declared. Clarification would be appreciated!

Evg
  • 25,259
  • 5
  • 41
  • 83
user137364
  • 305
  • 1
  • 7
  • It gets even more interesting if you throw some type inference in the mix: https://gcc.godbolt.org/z/7rzrMeE93 –  Jun 02 '22 at 18:56
  • @Frank that is indeed interesting - surprised that can even work. But do you know why it works in the first place? – user137364 Jun 02 '22 at 19:00
  • Suppose there is no base class and inside `D::bar()` you use `sizeof(D)`. This will compile. Is this surprising? But `sizeof` requires a complete type... Maybe we need a language-lawyer tag here as well. – Evg Jun 02 '22 at 19:00
  • @Evg: I agree. Just added one. – user137364 Jun 02 '22 at 19:01
  • 2
    My gut tells me this ultimately boils down to a class/struct only needing its member functions to be *declared* in order to be completely defined. The body of the functions being inlined in the class instead of being defined later is just a syntactical detail. There's probably a cleaner way to articulate that though. –  Jun 02 '22 at 19:05
  • 1
    @Frank: yeah, that would make sense. Perhaps in first reading the class, the compiler implicitly forward declares all the members before worrying about definitions? That might also explain some oddities with defining member functions in the interface. Member function f1 can call member function f2 before f2 is even declared. – user137364 Jun 02 '22 at 19:12
  • 1
    Consider this: http://eel.is/c++draft/class#mem.general-8 : *The class is regarded as complete within its complete-class contexts* and *A complete-class context of a class (template) is: a function body, ...* So inside `D::bar()`, `D` is a complete type. – Evg Jun 02 '22 at 19:13
  • @Evg: do the parentheses around "template" signify that the complete class context occurs for both class templates and ordinary classes with a function body? If so, I guess that's the answer since having a function body in D makes it complete. – user137364 Jun 02 '22 at 19:20
  • I guess so. I read it as "of a class or a class template". – Evg Jun 02 '22 at 19:23
  • @Evg: I guess there's one further point that may need to be understood about the incomplete types then. Consider struct X{ void foo() const {} X x; }; This gives a compiler error that type type X is incomplete. I agree it should not compile for other reasons (the class would need to construct an X which would in turn need to construct an X and so on). But the actual compiler error states that the field x cannot be set because X is incomplete. Having a function body, though, should make X complete. – user137364 Jun 02 '22 at 20:48
  • @user137364 Not the function body makes the class complete, but the function body is a complete-class context (among others), meaning that the class is still incomplete outside the function body! Regardless whether a function body exists. – Sebastian Jun 02 '22 at 21:03
  • @Sebastian: Thanks. I think I need to understand how the complete-class context terminology better. If D::baz is defined outside of the interface, the program still works. But in that situation, D::baz, when called where B::foo is instantiated, is not a complete-class context (not a function body) nor is D is complete. – user137364 Jun 02 '22 at 21:24
  • follow up: I think maybe the body of D::bar allows for D to be treated as a complete type (as per the "The class is regarded as complete within its complete-class contexts") and thus the B::foo calling D::baz is okay provided a declaration has been given. – user137364 Jun 02 '22 at 23:26

1 Answers1

2

Using B<D> as base class for D requires B<D> to be a complete class. Hence it will cause implicit instantiation of B<D>.

The point of instantiation of the class specialization is immediately before the namespace scope declaration requiring it, meaning before the definition of D. (by [temp.point]/4)

The implicit instantiation of B<D> does not cause implicit instantiation of the member function definition for foo. Hence D won't be required to be complete here.


The definition

void bar() const { foo(); }

has an ODR-use of foo. Therefore it will cause implicit instantiation of B<D>::foo.

The points of instantiation for a member function specialization are immediately after the namespace scope declaration and at the end of the translation unit. (by [temp.point]/1 and [temp.point]/7.1)

Both of these are after the definition of D and therefore D will be complete at these points. Consequently static_cast<const D*>(this)->baz(); is not a problem.


Note that D is not a template. The template instantiation rules do not apply to it. It doesn't matter whether bar is used or referred to at all. Neither does it matter whether you use D d; d.foo(); or anything at all following the definition of D.

user17732522
  • 53,019
  • 2
  • 56
  • 105