c++ constexpr typed as nested class
Asked Answered
H

2

7

This works: (A)

class Foo {
public:
  const bool b;
  constexpr ~Foo() = default;
  constexpr Foo(const bool b) : b(b) {};
};

class Bar {
public:
  static constexpr Foo tru { true };//Foo is complete type
};

This fails to compile: (B)

class Bar {
public:
  class Foo {
  public:
    const bool b;
    constexpr ~Foo() = default;
    constexpr Foo(const bool b) : b(b) {};
  };
  static constexpr Foo tru { true };//undefined constructor 'Foo' cannot be used
};

error:

$ clang++ --std=c++20 -D_POSIX_C_SOURCE=200112L -fPIC -g -Werror -Wall LiteralStruct.cpp -o LiteralStruct
LiteralStruct.cpp:9:24: error: constexpr variable 'tru' must be initialized by a constant expression
  static constexpr Foo tru { true };
                       ^~~~~~~~~~~~
LiteralStruct.cpp:9:24: note: undefined constructor 'Foo' cannot be used in a constant expression
LiteralStruct.cpp:7:15: note: declared here
    constexpr Foo(const bool b) : b(b) {};
              ^
1 error generated.

This also fails to compile, but gives a good reason: (C)

class Foo {
public:
  const bool b;
  constexpr ~Foo() = default;
  constexpr Foo(const bool b) : b(b) {};
  static constexpr Foo tru { true };//Foo is NOT a complete type
};

error:

$ clang++ --std=c++20 -D_POSIX_C_SOURCE=200112L -fPIC -g -Werror -Wall LiteralStruct.cpp -o LiteralStruct
LiteralStruct.cpp:6:24: error: constexpr variable cannot have non-literal type 'const Foo'
  static constexpr Foo tru { true };
                       ^
LiteralStruct.cpp:6:24: note: incomplete type 'const Foo' is not a literal type
LiteralStruct.cpp:1:7: note: definition of 'Foo' is not complete until the closing '}'
class Foo {

version:

clang version 10.0.0-4ubuntu1 
Target: x86_64-pc-linux-gnu

C failing makes sense and has a good error message. B feels like it should work, Foo and all it's contents should be complete and defined at that point in the file. Basically my question is: do I report a clang bug that B should work, or a feature request for a better error message? If Foo is truly not complete by virtue of being a member of an incomplete type, then I should think the error message should be similar to that of C.

Edit:

I just upgraded clang to the bleeding edge (16.0.0-++20221021052626+7dd2f4bc009d-1~exp1~20221021172738.418) and got the same result.

Heavyduty answered 22/10, 2022 at 13:57 Comment(10)
b(b) is doubtful and probably confuse the compilerSimitar
@Simitar No, it's perfectly clear what that means. The compiler will not have a problem with that.Barbabas
I don't find a proper duplicate right now but afaik, the nested class is not considered fully defined/complete until the outer class is, and that would be the reason for the error.Barbabas
I guess this more of a specification issue than anything. The problem is that the body of the constructor of Foo is considered a complete-class context, which effectively means that names in it are supposed to be looked up as if the definition was placed after the closing } of the enclosing class. With that in mind the error message makes perfect sense. However, I don't think there is actually anything in the standard at the moment saying that this is also true for other purposes, like the question of whether the function is defined at the point of the constant expression.Mazur
See CWG issue 1255. In practice to make the complete-class rules work, I guess there is no other way than to treat the definition of the constructor as appearing only after the closing } of the enclosing class, also for the purpose of constant expression evaluation.Mazur
@JasonLiam Yes Foo is complete after its closing }, so changing the error message to state that the class is incomplete would not be appropriate. However, complete-class context also applies to the enclosing class. So the definition of the constructor is supposed to lookup names as if both Foo and Bar were complete. Practically that means moving the definition to after the closing } of the outer-most class. And with that implementation you can see why the constructor is undefined at the point of the constant expression.Mazur
The issue is not completeness of the type, but definedness of the function used in the constant expression. (A function called in a constant expression must be defined before the expression.)Mazur
Edit: nvm. Bar can access other children at the time of constexpr initialization. Tested with constexpr pointer to member function. <strike>Oh duh. Foo tru { true }; in the context of B is shorthand (except for the implicit copy) for Bar::Foo tru = Bar::Foo(true); and Bar is incomplete so Bar::Foo can't be accessed.</strike>Heavyduty
@Heavyduty No. These two declarations are not generally equivalent and secondly lookup for already declared names is allowed even if the class isn't complete yet (i.e. still being defined). Otherwise you wouldn't be able to have declarations depend on one another at all in a class. The problem here is specifically about whether the compiler knows the body of the constructor that you want to use in the static constexpr Foo tru { true }; declaration at this point. Intuitively it seems like it obviously does, as you defined it above, but with the complete-class context rules it is non-obvious.Mazur
@JasonLiam Because that is what the name lookup rules imply. See eel.is/c++draft/class#mem.general-7 which doesn't require the listed constructs to be part of members of the class for which the complete-class context is considered. It only says "within the member-specification", which includes nested classes. The note following also makes that clear.Mazur
M
3

The problem with (B) is distinct from the one with (C). In (B) the completeness of Foo is not in question. Foo is complete as soon as the closing } of its definition is reached and since the member declaration of tru is placed after that, Foo is complete for its purpose.

There is also no problem in (B) with Bar being incomplete at the declaration of tru. While that's true, incompleteness does not prevent looking up members which were declared prior, like the nested class Foo.

The problem is, as the error message is pointing out, whether or not the constructor Foo::Foo(bool) is defined at the point of the declaration of tru. This is an important question, because tru is initialized in that declaration using the constructor in question and it is marked constexpr, requiring that the initialization be a constant expression. Calling a (constexpr) function in a constant expression is only allowed if the function is defined (not only declared) prior to the expression.

Consider for example

class Bar {
public:
  class Foo {
  public:
    const bool b;
    constexpr ~Foo() = default;
    constexpr Foo(const bool b);
  };
  static constexpr Foo tru { true };
};

constexpr Bar::Foo::Foo(const bool b) : b(b) {};

You will get the same or a similar error message here. In this case it is more obvious what the issue is. When static constexpr Foo tru { true }; is reached and the compiler tries to evaluate the (compile-time constant) value of Foo, it hasn't seen the definition of the constructor yet, so it can't know how to determine the value of tru.

Now in your example (B) it seems that Bar::Foo::Foo(bool) is defined before it is used in the constant expression for tru and I think if one follows the current standard by exact wording, then this is true. However, there is a complication which changes this in practice and in the probable intent of the standard:

The body of a function defined inside a class is special in that it is a so-called complete-class context. In such a context it is possible for name lookup to find not only preceding declarations as is normally the case in C++, but also declarations for all members of the class (and enclosing classes), irregardless of whether they are declared only later.

So for example the following is allowed:

class Bar {
public:
  class Foo {
  public:
    const bool b;
    ~Foo() = default;
    Foo(const bool b) : b(X) {};
  };
  constexpr static bool X = false;
};

Although X is not declared yet when Foo::Foo(bool) is defined and uses X, the compiler has to accept it and figure out that X is the static member declared at the end.

In order to achieve this lookup behavior, the compiler practically must rewrite the code to

class Bar {
public:
  class Foo {
  public:
    const bool b;
    ~Foo() = default;
    Foo(const bool b);
  };
  constexpr static bool X = false;
};

inline Bar::Foo(const bool b) : b(X) {};

Now "normal" lookup rules can find X as expected.

But if we apply this rewriting to your example (B) we get my first example in this answer and as we determined it cannot work with the constant expression evaluation. So in practice you can't use a member function of the same class (including nested or enclosing classes) in a constant expression evaluation for a static data member inside the class definition itself.

That the current standard wording doesn't describe this behavior properly is an issue with the standard, not the compiler. My impression is that Clang is implementing it as intended. While it may sometimes be possible to consider the constructor defined where it is lexically placed for the purpose of constant expression evaluation if e.g. all names used in its definition can already be found at that point, in general it is impossible, because you could create infinite recursive type dependencies this way.

See e.g. the active CWG issue 1255 and CWG issue 1626.

Mazur answered 22/10, 2022 at 15:33 Comment(0)
A
0

The first thing to note is that a complete-class context of a nested class is also a complete-class context of any enclosing class, if the nested class is defined within the member-specification of the enclosing class as per mem.general#7.

This combined with the fact that the body of the constructor Bar::Foo::Foo(const bool) is a complete-class context means that the names appearing inside the body of Bar::Foo::Foo(const bool) will be lookup up as if the definition of that ctor is placed after the enclosing class' }.

This in turn implies that at the point where you have the definition/declaration of the constexpr data member tru, the constructor Bar::Foo::Foo(const bool) is not yet defined. But this violates expr.const#5 which states:

5. An expression E is a core constant expression unless the evaluation of E, following the rules of the abstract machine ([intro.execution]), would evaluate one of the following:

  • 5.2 an invocation of an undefined constexpr function;

(emphasis mine)

This is exactly what the error of one of the compiler says says:

 error: constexpr variable 'tru' must be initialized by a constant expression
Alialia answered 22/10, 2022 at 15:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.