正在定义的类的静态constexpr成员与该类相同的类型。

48

我想让一个类C拥有一个类型为C的静态constexpr成员。在C++11中是否可能实现?

尝试1:

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

g++ 4.7.0 报错:'invalid use of incomplete type',指的是对 Foo() 的调用。

尝试2:

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

现在问题是类定义中缺少constexpr成员f的初始化器。

尝试3:

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

现在g++抱怨Foo::f的重新声明与constexpr不同。

5个回答

37
如果我正确解释了标准,这是不可能的。
从上述内容中(以及在静态数据成员声明中没有单独关于非文字类型的说明),我认为可以推导出,一个被constexpr修饰的静态数据成员必须是文字类型(如§3.9/10所定义的),并且它必须在声明中同时有定义。后者的条件可以通过使用以下代码来满足:
struct Foo {
  constexpr Foo() {}
  static constexpr Foo f {};
};

这段代码类似于你的Attempt 1,但没有使用类外定义。

然而,由于在静态成员声明/定义时Foo是不完整的,编译器无法检查它是否是字面类型(如§3.9 / 10中定义的),因此拒绝了该代码。

请注意,有这篇C++11之后的文档(N3308),其中讨论了当前标准中constexpr定义的各种问题,并提出了修正建议。特别是,“拟议措辞”部分建议修改§3.9 / 10,意味着包含不完整类型作为字面类型之一。如果该修正案被接受并列入未来版本的标准,则可以解决您的问题。


2
+1 感谢您及时向我们提供讨论的最新进展并回答问题! - Matthieu M.
3
我刚刚遇到了这个确切的问题。它似乎与static const与不完整类型的使用方式不一致。我想现在只能暂时使用static const了! - Matt Clarkson
1
这并不是严格错误,但不如Richard Smith的答案有帮助。将其声明为const,然后在下面定义为constexpr。请注意 - 这现在适用于内联定义以及constexpr。 - lewis
这在C++20中有改变吗?您知道修正建议的状态吗? - CAD97

19
我认为GCC拒绝你的第三次尝试是不正确的。C++11标准(或其接受的缺陷报告)中没有规定,如果先前的声明是constexpr,则变量的重新声明必须是constexpr。标准最接近这个规则的地方在[dcl.constexpr](7.1.5)/1_中:

如果函数或函数模板的任何声明具有constexpr说明符,则其所有声明都应包含constexpr说明符。

Clang对constexpr的实现接受了你的第三次尝试。

1
也许是这样,但在第三次尝试中,该变量仅为const而不是constexpr,对吗? - Ben Voigt
在第三次尝试中,变量是constexpr的,因为在定义中指定了constexpr。标准的相关部分是5.19/2:“可以将lvalue-to-rvalue转换[应用于]引用非易失性对象的文字类型的非易失性glvalue *定义为constexpr*”。 - Richard Smith
你确定这是允许的吗?静态成员变量不能在类外引入,它必须首先在类内用相同类型(包括cv-qualifiers)进行声明。我似乎找不到需要这样做的规则,但为了查看是否将constexpr排除在签名匹配之外,这是必要的。 - Ben Voigt
好的,规则的一部分在8.3p1中:“当declarator-id被限定时,声明应该引用先前声明的类或命名空间的成员,以便限定符引用”。我仍然找不到指定类型和限定符匹配程度的细节。 - Ben Voigt
1
你正在寻找3.5p10:“在类型的所有调整之后(其中typedefs(7.1.3)被其定义替换),所有引用给定变量或函数的声明指定的类型应该是相同的,除了数组对象的声明可以指定不同的数组类型,这些类型的区别在于是否存在主要数组边界”。请注意,constexprconst添加到类型中,但它本身不是类型的一部分。 - Richard Smith
请注意,这种方法同样适用于“inline” - 就像“constexpr”一样。 - lewis

14

关于Richard Smith的回答的更新,第三次尝试现在可以在GCC 4.9和5.1以及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;

然而,当Foo是一个类模板时,clang 3.4会失败,但GCC 4.9和5.1仍然可以正常工作。
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: non-type template argument is not a constant expression
std::array<int, Foo<std::size_t>::f.v> a;
                ^~~~~~~~~~~~~~~~~~~~~

3
不幸的是,如果您将定义放在.h文件中,在链接时会出现“多重定义”错误。而如果您将其放在C++文件中,则其他C++文件不知道它是constexpr。 - Martin C. Martin
1
@MartinC.Martin 这就是使用模板的原因,或者如果使用C++17,您可以使用“inline”变量。然而,Clang和MSVC目前在将模板用作编译时常量表达式时会出现问题。使用内联变量,这在GCC、Clang和MSVC上都可以工作(当然,要使用相应版本)。 - monkey0506
2
@monkey0506,我遇到了类似的问题。您能否展示如何使用C++17内联变量来解决这个问题? - Filip S.

1
早些时候我也遇到了同样的问题,并发现了这个十年前的问题。很高兴地报告,在这些年里出现了一个解决方案;我们只需要像上面的“尝试3”那样做,但将Foo :: f 的定义标记为inline。最小示例可使用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; }

这似乎不是可移植的。它可以在GCC和Clang上工作,但MSVC会出现“error LNK2005:在...中已经定义了public:static class ...”的错误。https://learn.microsoft.com/en-us/cpp/error-messages/tool-errors/linker-tools-error-lnk2005 - aij
@aij 我刚刚在 Visual Studio 上试过了,对我来说编译正常。就像对于 gcc/clang 需要通过 (在解决方案资源管理器中右键单击项目) -> 属性 -> 配置属性 -> C/C++ -> 语言 -> C++ 语言标准 传递 /std:c++17 一样。 - Daniel McLaury
顺便提一下:#define __FOO_HPP 会引发未定义的行为,因为您不允许在符号中使用双下划线(同样适用于前导下划线后跟大写字母)。https://en.cppreference.com/w/cpp/language/identifiers (我99.9%确定这也适用于宏名称。) - Ben

0

如果你像我一样,试图制作类似于enum的类,我最好想到的方法是使用CRTP将行为放入基类中,然后派生类是存在的“真实”类,仅具有“枚举器”类似的值作为static constexpr inline Base成员。这意味着Foo::yes不是Foo类型,但它的行为像Foo并且可以隐式转换为Foo,所以它看起来非常接近。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} {}    
};

唯一的问题是,如果你开始使用decltype(Foo::yes),就好像它是一个Foo一样。

为什么不直接使用嵌套类呢?就像这样:https://dev59.com/sF0b5IYBdhLWcg3wO_I_#70048197 - 303
这有点帮助,因为它在逻辑上是封闭类的一部分,而且你不需要提前声明任何东西,但是...我只想让静态成员像enum一样是相同类型。 - Ben

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