Home > other >  c constexpr typed as nested class
c constexpr typed as nested class

Time:10-23

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.

CodePudding user response:

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.

CodePudding user response:

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(const bool) is a complete-class context means that the names appearing inside the body of Bar::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(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
  • Related