由于模板基类,从不完整的类型初始化静态constexpr

5
我有一个模板基类,期望子类将自己作为模板参数传递。
它看起来有点像这样:
template<typename T>
struct Base {
    constexpr Base(int x) : m_x(x) {}
private:
    int m_x;
};

struct Derived : public Base<Derived>
{
    static const Derived LIFE;
    constexpr Derived(int x) : Base(x) {}
};

const Derived Derived::LIFE = Derived(42);

这段话的意思是:“编译和运行都符合预期。但现在我想让Derived :: LIFE成为constexpr。这可能吗?我不能只将其const限定符更改为constexpr,因为constexpr需要在声明时初始化。”
test.cpp:10:28: error: constexpr static data member ‘LIFE’ must have an initializer
   static constexpr Derived LIFE;

我无法在那里初始化它,因为Derived是不完整的类型:
test.cpp:10:45: error: invalid use of incomplete type ‘struct Derived’
   static constexpr Derived LIFE = Derived(42);

我意识到,如果Derived是一个完整的类型,这个问题就会消失,但在这种情况下,出于与本问题无关的原因,我对自引用模板基类非常依赖。

如果我正确理解this answer中的最后一段,似乎至少有一些讨论将来如何处理不完整类型,但这并不能帮助我解决眼前的问题。

是否有人知道一些技巧可以推迟我的上述代码中LIFE的初始化?


我有一个模板基类,子类需要将自己作为模板参数传递。这被称为“奇异递归模板模式”(Curiously Recurring Template Pattern)。 - dyp
感谢@dyp:我一直想为这个模式取一个名字。所以我想显而易见的(但不太美观)解决方案是将常量移出其类。我怀疑在我的情况下重新组织事物使类型完整并不实际。 - Martin
1
阅读了链接的答案和标准后,我仍然认为我的第二个(现已删除)评论是正确的:由于Derived是在其“作用域”内的不完整类型(名称查找被推迟到类定义之后),因此您可能无法创建其类型的constexpr对象作为静态数据成员。但是,类的成员函数的定义不在其作用域内。因此,您应该能够使用类似于static constexpr Derived get_LIFE() { return {42}; }的东西。 - dyp
2个回答

10

您可以在LIFE的定义中简单地添加constexpr

constexpr Derived Derived::LIFE = Derived(42);

最近GCC存在一个错误,会拒绝这个操作;您需要使用Clang或者GCC 4.9。


那么,即使它不是声明的一部分,你也可以在定义中使用 constexpr 吗?这看起来很有趣,它在哪里指定了呢? - Johannes Schaub - litb
@litb 没有规定说你不能这样做。7.1.5/1说:“constexpr说明符只能应用于变量的定义[...]。如果任何函数或函数模板的声明具有constexpr说明符,则其所有声明都必须包含constexpr说明符。”因此,变量声明不能具有该说明符,但是与函数不同,变量重新声明不需要具有匹配的constexpr说明符。 - Richard Smith
1
我已经使用GCC([ideone](https://ideone.com/r8pHW5)),clang([rextester](http://rextester.com/SBC68067))和MSVC 2017进行了测试,虽然IntelliSense对此并不太关心,但在这三个编译器上都能按预期工作。我通过将它们作为std :: array的大小来访问static constexpr成员,以确保它们被视为编译时常量表达式。 - monkey0506
这在ICC上不起作用:https://gcc.godbolt.org/z/aquzGx 但仍然+1。 - Yankes
没有规定说你不能这样做。这种推理让我感到不安,因为如果也没有规则指定在这种情况下实现必须做什么,那么它不是未定义行为吗?由于@RichardSmith说这是一个错误,我猜想它实际上在某个地方被定义了。 - aij

4
我认为你应该使用惰性初始化。实际上,Derived仍然是不完整的类型;因为编译器还不知道它的大小。

所以代码应该是:

struct Derived : public Base<Derived>
{
    constexpr Derived(int x) : Base(x) {}

    static constexpr Derived getLIFE()
    {
        return Derived(34);
    }
};

编辑: 使用以下代码段可以复现相同的不完整类型行为:

struct MyStruct
{
    static constexpr int x = sizeof(MyStruct);
};

啊,是的 - 这很好用。非常感谢你和@dyp。 - Martin
我也有这个想法,但它与 OP 的方法不同,即每次调用它都会产生一个新的对象。然而,当你有一个常量表达式时,对象身份的概念变得有点无意义。 - Kerrek SB

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