Why can't I use the result of a static constexpr in the declaration of another function in a class?

Viewed 442

This is my barebones code:

#include <iostream>
#include <array>

class cColor {
  public:
  enum eValue { k_Red, k_Green, k_Blue };
  static constexpr std::size_t NumValues() { return 3; }
  static constexpr std::array<eValue, NumValues()> Values()  { return {k_Red, k_Green, k_Blue}; }
};

int main() {
  std::cout << "NumColors=" << cColor::NumValues() << '\n';
}

I'm trying to declare Values() as a static constexpr and I thought I should be able to use NumValues() as it's a static constexpr as well. However, this program fails to compile and throws this error:

main.cpp:8:39: error: non-type template argument is not a constant expression
  static constexpr std::array<eValue, NumValues()> Values()  { return {k_Red, k_Green, k_Blue}; }
                                      ^~~~~~~~~~~
main.cpp:8:39: note: undefined function 'NumValues' cannot be used in a constant expression
main.cpp:7:32: note: declared here
  static constexpr std::size_t NumValues() { return 3; }

However, if I use a static constexpr member variable it works just fine.

#include <iostream>
#include <array>

class cColor {
  public:
  enum eValue { k_Red, k_Green, k_Blue };
  static constexpr std::size_t NumValues {3};
  static constexpr std::array<eValue, NumValues> Values()  { return {k_Red, k_Green, k_Blue}; }
};

int main() {
  std::cout << "NumColors=" << cColor::NumValues << '\n';
}

So what is it about a static constexpr member function that prevents the code from compiling?

1 Answers

It's because it's in a class definition. You can't use the static functions of a class at compile time until after the full definition of the class.

If the reason is unclear, your code is actually this:

class cColor {
  public:
  enum eValue { k_Red, k_Green, k_Blue };
  static constexpr std::size_t NumValues() { return 3; }
  static constexpr std::array<cColor::eValue, cColor::NumValues()> Values()  { return {k_Red, k_Green, k_Blue}; }
};

You see, when you say std::array<cColor::eValue, cColor::NumValues()> as the return type, you're using cColor::NumValues(), which uses cColor which has not yet been defined (because it's inside the class definition.

You're effectively defining a component of cColor in terms of itself.

The problem is solved by moving the self-referential component outside of the class (one or both of them):

#include <iostream>
#include <array>

static constexpr std::size_t NumValues() { return 3; }

class cColor {
  public:
  enum eValue { k_Red, k_Green, k_Blue };

  static constexpr std::array<eValue, NumValues()> Values()  { return {k_Red, k_Green, k_Blue}; }
};

int main() {
  std::cout << "NumColors=" << NumValues() << '\n';
}

Edit:

To further answer your question as to why specifically the use of a constexpr function is causing problems (as opposed to your revised question using a constexpr variable), I'll give this next piece of information:

Have you ever noticed that you can't use a function before it's declared, but you can use a member function before it's declared (in the class definition I mean).

That's because the C++ compiler glosses over the entire class, including the member/static variables and methods/static methods before it does anything else. Therefore the methods/static methods (which I'll collectively refer to as member functions) must have well-formed declarations prior to having any of their actual implementation defined - this of course includes their return types.

Thus, at compile time, when the declaration for Values() is examined, it knows the return type depends on NumValues(), and it knows NumValues() returns std::size_t, but it hasn't yet examined the implementation of any member functions in the class yet. Thus it doesn't know (yet) that NumValues() will return 3.

Thus, you can also solve this problem by using delayed return type deduction. The real crux of the problem is that Values() must have a well-formed return type before the implementation of its class's member functions are inspected.

Here's another solution that might illuminate the specifics of the problem:

#include <iostream>
#include <array>

class cColor {
  public:
  enum eValue { k_Red, k_Green, k_Blue };
  static constexpr std::size_t NumValues() { return 3; }
  static constexpr auto Values() { return std::array<eValue, NumValues()>{k_Red, k_Green, k_Blue}; }
};

int main() {
  std::cout << "NumColors=" << cColor::NumValues() << '\n';
}

You see, auto is a valid return type for the signature, and the actual return type is deduced from the method implementation, which at that point knows the implementation of NumValues().

The reason for this weird compiler parsing order is so that you don't have to arrange methods in a certain order for them to compile (under normal circumstances - read on). This way all methods are known prior to any implementations, which is sort of like having a forward declaration for every method in the class.

And just if you were wondering, yes, moving the definition/declaration of NumValues() to be after Values() will cause a compilation failure because our trick doesn't work anymore since the implementation for NumValues() is examined after the implementation for Values() and thus Values() doesn't know NumValues() returns 3:

class cColor {
  public:
  enum eValue { k_Red, k_Green, k_Blue };

  // THIS ORDERING FAILS TO COMPILE
  static constexpr auto Values() { return std::array<eValue, NumValues()>{k_Red, k_Green, k_Blue}; }
  static constexpr std::size_t NumValues() { return 3; }
};

Your example works because constexpr variables must be defined and declared at the same time, so the value of 3 is known at that point onward and thus makes the declarations valid. However, if you move the declaration/definition of the static constexpr member variable to be after Values() you again have a compile error which can be fixed by using the auto hack I demonstrated above.

class cColor {
  public:
  enum eValue { k_Red, k_Green, k_Blue };

  // THIS ORDERING FAILS TO COMPILE
  static constexpr std::array<eValue, NumValues> Values() { return {k_Red, k_Green, k_Blue}; }
  static constexpr std::size_t NumValues = 3;
};
class cColor {
  public:
  enum eValue { k_Red, k_Green, k_Blue };

  // AUTO TRICK MAKES THIS WORK
  static constexpr auto Values() { return std::array<eValue, NumValues>{k_Red, k_Green, k_Blue}; }
  static constexpr std::size_t NumValues = 3;
};
Related