为什么我不能在类的另一个函数声明中使用静态constexpr的结果?

5
这是我的基本代码:
#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';
}

我试图将Values()声明为静态constexpr,并且认为我应该能够使用NumValues(),因为它也是一个静态constexpr。然而,这个程序编译失败了,并抛出了这个错误:

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; }

然而,如果我使用一个静态的constexpr成员变量,它就可以正常工作。请参见这里
#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';
}

那么,静态constexpr成员函数是什么导致代码无法编译?

1个回答

6

这是因为它在类定义中。在完整定义类之前,您无法在编译时使用类的静态函数。

如果原因不明确,那么您的代码实际上是这样的:

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}; }
};

您看,当您将std :: array < cColor :: eValue,cColor :: NumValues()>作为返回类型时,您正在使用cColor :: NumValues(),它使用尚未定义的cColor(因为它位于类定义内部)。

您实际上是在使用自己定义的cColor的一部分来定义另一个部分。

通过将自引用组件移出类(其中的一个或两个),问题得到解决:

#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';
}

编辑:

为了进一步回答你关于为什么使用constexpr函数会导致问题(而不是使用constexpr变量的修订后问题),我会给出下面的信息:

你是否注意到在声明之前不能使用一个函数,但可以在成员函数声明之前使用它呢?(我是指在类定义中。)

这是因为C++编译器在执行任何其他操作之前都会快速扫描整个类,包括成员/静态变量、方法/静态方法(我将其统称为成员函数)。因此,在定义实现之前,成员函数必须具有良好的声明 - 当然也包括它们的返回类型。

因此,在编译时,当检查Values()的声明时,它知道其返回类型取决于NumValues(),并且知道NumValues()返回std::size_t,但它尚未检查类中任何成员函数的实现。 因此,它还不知道NumValues()将返回3

因此,您还可以通过使用延迟返回类型推断来解决此问题。实际上问题的关键是,在检查其类成员函数实现之前,Values()必须具有良好的返回类型。

以下是另一种解决方案,可能会阐明该问题的具体情况:

#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';
}

你看,auto是一个有效的函数签名返回类型,实际的返回类型会从方法的具体实现中推导出来,在此之前,方法知道NumValues()的实现。
这种奇怪的编译器解析顺序的原因是为了让你不必按照特定顺序排列方法才能编译(在正常情况下 - 接下来请阅读)。这样一来,在任何实现之前都已经知道所有的方法,有点像在类中为每个方法提供前置声明。
如果你好奇的话,移动NumValues()的定义/声明放在Values()之后将会导致编译失败,因为我们的技巧不再起作用,因为NumValues()的实现在Values()的实现之后被检查,因此Values()不知道NumValues()返回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; }
};

你的例子可以工作是因为constexpr变量必须同时定义和声明,因此在那个时间点上知道了“3”的值,从而使这些声明有效。但是,如果你将静态constexpr成员变量的声明/定义移动到Values()之后,你又会遇到一个编译错误,可以通过使用我上面演示的auto hack来解决。
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;
};

啊,这样说就有道理了。谢谢。 - kshenoy
哦,第二个输出里的“未定义函数”没关系 :D - Lightness Races in Orbit
1
@kshenoy 好的,我已经更新了我的答案,并提供了针对该问题的具体信息。那实际上是一个非常重要的点,我最初忘记包括它(也就是“为什么”)。 - Cruz Jean
感谢你的详细回答,Jean。抱歉在你的回答下面更改了原来的问题,但我认为它们看起来很相关,所以不想将它们拆成两个单独的函数。 - kshenoy
@kshenoy 嗯,你说得对,第一次我可能应该包含一个替代重新定位函数的方法。现在我认为它有一些很好的替代方案了。祝你 C++ 编程愉快! - Cruz Jean
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接