48

I would like a class C to have a static constexpr member of type C. Is this possible in C++11?

Attempt 1:

struct Foo {
    constexpr Foo() {}
    static constexpr Foo f = Foo();
};
constexpr Foo Foo::f;

g++ 4.7.0 says: 'invalid use of incomplete type' referring to the Foo() call.

Attempt 2:

struct Foo {
    constexpr Foo() {}
    static constexpr Foo f;
};
constexpr Foo Foo::f = Foo();

Now the problem is the lack of an initializer for the constexpr member f inside the class definition.

Attempt 3:

struct Foo {
    constexpr Foo() {}
    static const Foo f;
};
constexpr Foo Foo::f = Foo();

Now g++ complains about a redeclaration of Foo::f differing in constexpr.

jogojapan
  • 68,383
  • 11
  • 101
  • 131
ndkrempel
  • 1,906
  • 14
  • 18

5 Answers5

37

If I interpret the Standard correctly, it isn't possible.

(§9.4.2/3) [...] A static data member of literal type can be declared in the class definition with the constexpr specifier; if so, its declaration shall specify a brace-or-equal-initializer in which every initializer-clause that is an assignment-expression is a constant expression. [...]

From the above (along with the fact that there is no separate statement about non-literal types in static data member declarations), I believe it follows that a static data member that is constexpr must be a literal type (as defined in §3.9/10), and it must have its definition included in the declaration. The latter condition could be satisfied by using the following code:

struct Foo {
  constexpr Foo() {}
  static constexpr Foo f {};
};

which is similar to your Attempt 1, but without the class-external definition.

However, since Foo is incomplete at the time of declaration/definition of the static member, the compiler can't check whether it is a literal type (as defined in §3.9/10), so it rejects the code.

Note that there is this post-C++-11 document (N3308) which discusses various problems of the current definition of constexpr in the Standard, and makes suggestions for amendments. Specifically, the "Proposed Wording" section suggests an amendment of §3.9/10 that implies the inclusion of incomplete types as one kind of literal type. If that amendment was to be accepted into a future version of the Standard, your problem would be solved.

jogojapan
  • 68,383
  • 11
  • 101
  • 131
  • 2
    +1 for keeping us abreast of the current state of discussions on top of prodiving the answer! – Matthieu M. Aug 13 '12 at 08:52
  • 3
    I've just come up against this exact problem. It seems inconsistent with how `static const` can work with incomplete types. I guess I'll just have to make do with a `static const` for now! – Matt Clarkson Aug 29 '12 at 18:09
  • 1
    This is not strictly WRONG, but not as helpful an answer as the one from Richard Smith. Declare as const and then define just below as constexpr. AND - please note - this now applies to inline defitions as well as constexpr – lewis Aug 19 '20 at 21:26
  • Has this changed with C++20? Do you know the status of the amendment suggestions? – CAD97 Aug 28 '20 at 01:26
19

I believe GCC is incorrect to reject your Attempt 3. There is no rule in the C++11 standard (or any of its accepted defect reports) which says that a redeclaration of a variable must be constexpr iff the prior declaration was. The closest the standard comes to that rule is in [dcl.constexpr](7.1.5)/1_:

If any declaration of a function or function template has constexpr specifier, then all its declarations shall contain the constexpr specifier.

Clang's implementation of constexpr accepts your Attempt 3.

Richard Smith
  • 13,696
  • 56
  • 78
  • 1
    Perhaps, but in Attempt 3, the variable is only `const` and not a `constexpr`, isn't it? – Ben Voigt Sep 24 '12 at 04:06
  • In Attempt 3, the variable is `constexpr`, because `constexpr` was specified in the definition. The relevant section of the standard is 5.19/2: "an lvalue-to-rvalue conversion [...may be applied to...] a non-volatile glvalue of literal type that refers to a non-volatile object *defined with `constexpr`* – Richard Smith Sep 27 '12 at 04:38
  • Are you sure that's allowed? A static member variable can't be introduced outside the class, it first has to be declared inside the class with the same type (including *cv-qualifiers*). I couldn't seem to find the rule which requires that, however, in order to see whether `constexpr` is excluded from the signature matching. – Ben Voigt Sep 27 '12 at 05:21
  • Ok, part of the rule is in 8.3p1: "When the *declarator-id* is qualified, the declaration shall refer to a previously declared member of the class or namespace to which the qualifier refers" I still can't find the bits which specify how well the type and qualifiers have to match. – Ben Voigt Sep 27 '12 at 05:30
  • 1
    You're looking for 3.5p10: "After all adjustments of types (during which typedefs (7.1.3) are replaced by their definitions), the types specified by all declarations referring to a given variable or function shall be identical, except that declarations for an array object can specify array types that differ by the presence or absence of a major array bound". Note that `constexpr` adds `const` to the type, but is not itself part of the type. – Richard Smith Sep 29 '12 at 04:33
  • Please note this approach ALSO works for 'inline' - as for 'constexpr' – lewis Aug 19 '20 at 21:27
14

An update on Richard Smith's answer, attempt 3 now compiles on both GCC 4.9 and 5.1, as well as clang 3.4.

struct Foo {
  std::size_t v;
  constexpr Foo() : v(){}
  static const Foo f;
};

constexpr const Foo Foo::f = Foo();

std::array<int, Foo::f.v> a;

However, when Foo is a class template, clang 3.4 fails, but GCC 4.9 and 5.1 still work ok:

template < class T >
struct Foo {
  T v;
  constexpr Foo() : v(){}
  static const Foo f;
};

template < class T >
constexpr const Foo<T> Foo<T>::f = Foo();

std::array<int, Foo<std::size_t>::f.v> a; // gcc ok, clang complains

Clang error :

error: non-type template argument is not a constant expression
std::array<int, Foo<std::size_t>::f.v> a;
                ^~~~~~~~~~~~~~~~~~~~~
Community
  • 1
  • 1
xavlours
  • 743
  • 7
  • 11
  • 3
    Unfortunately, if you put the definition in your .h file, you end up with "multiple definition" errors at link time. And if you put it in your C++ file, other C++ files don't know that it is constexpr. – Martin C. Martin Nov 14 '17 at 21:24
  • 1
    @MartinC.Martin That's the reason for using the template, or if using C++17 you can use `inline` variables. Clang and MSVC currently choke on using the template as a compile-time constant expression though. Using inline variables, this works on GCC, Clang, and MSVC (relevant versions of each, of course). – monkey0506 May 21 '18 at 01:12
  • 2
    @monkey0506 I'm having a similar issue as this. Could you show how to do this using C++17 inline variables? – Filip S. Oct 02 '19 at 07:59
1

Earlier I had the same problem and came across this decade-old question. I'm happy to report that in the intervening years a solution has appeared; we just need to do something like "attempt 3" above, but mark the definition of Foo::f as inline. Minimal example which compiles with g++ --std=c++17:

foo.hpp

#ifndef __FOO_HPP
#define __FOO_HPP

struct Foo
{
    constexpr Foo() {}
    static const Foo f;
};

inline constexpr Foo Foo::f = Foo();

#endif

foo.cpp

#include "foo.h"

main.cpp

#include "foo.h"

int main(int, char **) { return 0; }
Daniel McLaury
  • 4,047
  • 1
  • 15
  • 37
  • This doesn't seem to be portable. It works with GCC and Clang, but MSVC fails with `error LNK2005: "public: static class ... already defined in ...` https://learn.microsoft.com/en-us/cpp/error-messages/tool-errors/linker-tools-error-lnk2005 – aij Nov 29 '21 at 16:18
  • @aij I just tried this right now on Visual Studio and it built fine for me. Like for gcc/clang you need to pass `/std:c++17` to MSVC, which I did via (right click on project in Solution Explorer) -> Properties -> Configuration Properties -> C/C++ -> Language -> C++ Language Standard – Daniel McLaury Nov 29 '21 at 18:09
  • BTW: `#define __FOO_HPP` invokes undefined behavior because you aren't allowed to use double underscores in your symbols (same with leading underscore followed by a capital). https://en.cppreference.com/w/cpp/language/identifiers (I'm 99.9% true this applies to macro names as well.) – Ben Mar 15 '23 at 21:05
0

If, like me, you are trying to make enum-like classes, the best I've figured out is to use CRTP to put the behavior in a base class and then a derived class is the "real" class that exists just to have the "enumerator"-like values as static constexpr inline Base members. This means that Foo::yes isn't of type Foo, but it acts like Foo and is implicitly convertible to Foo, so it seems pretty close. https://godbolt.org/z/rTEdKxE3h

template <class Derived>
class StrongBoolBase {
public:
    explicit constexpr StrongBoolBase() noexcept : m_val{false} {}
    explicit constexpr StrongBoolBase(bool val) noexcept : m_val{val} {}

    [[nodiscard]] constexpr explicit operator bool() const noexcept { return m_val; }

    [[nodiscard]] constexpr auto operator<=>(const StrongBoolBase&) const noexcept = default;
    [[nodiscard]] constexpr auto operator not() const noexcept {
        return StrongBoolBase{not this->asBool()};
    }
    [[nodiscard]] constexpr bool asBool() const noexcept {
        return m_val;
    }

private:
  bool m_val;
};

template <class Tag>
class StrongBool : public StrongBoolBase<StrongBool<Tag>> {
    using Base = StrongBoolBase<StrongBool<Tag>>;
public:
    //////// This is the interesting part: yes and no aren't StrongBool:
    inline static constexpr auto yes = Base{true};
    inline static constexpr auto no = Base{false};
    using Base::Base;
    constexpr StrongBool(Base b) noexcept : Base{b} {}    
};

The only breakdown is if you start to use decltype(Foo::yes) as though it's a Foo.

Ben
  • 9,184
  • 1
  • 43
  • 56
  • Why not simply use a nested class? Like so: https://stackoverflow.com/a/70048197/7107236 – 303 Apr 08 '23 at 02:04
  • That helps a little, since it’s logicLly part of the enclosing class and since you don’t have to forward declare anything, but still… I just want the static member to be the same type like `enum` can do. – Ben Apr 09 '23 at 03:04