这个应该是 constexpr 吗?

7

考虑以下代码片段(godbolt):

#include <cstdio>
#include <string>
#include <string_view>

struct Option
{
    std::string_view name;

    constexpr Option( std::string_view const n ) noexcept : name{n} {}
};

template< std::size_t N >
class TransformedOption : public Option
{
public:
    constexpr TransformedOption( std::string_view const nameStr ) :
        Option{ { nameStorage_, N - 1 } }
    {
        for ( auto i = 0U; i < N; ++i )
        {
            if ( nameStr[ i ] == '_' ) { nameStorage_[ i ] = '-'; }
            else                       { nameStorage_[ i ] = nameStr[ i ]; }
        }
    }
    private:
        char nameStorage_[ N ] = {};
};

template< std::size_t N >
constexpr TransformedOption< N > make( char const (&nameStr)[ N ] ) noexcept
{
    return TransformedOption< N  >{ nameStr };
}

int main()
{
    /*constexpr*/ auto t = make( "abcd_efgh_ijkl_mnop_peqst" );
    std::printf( "%s\n", t.name.data() );
    return 0;
}

基本上,我想通过将每个 _ 替换为 - 并确保最终的二进制文件仅包含转换后的字符串(而不是原始字符串)来执行编译时字符串转换。
我尝试过 Clang 10.0.1、GCC 10.2 和 MSVC 19.24(请参见上面的 godbolt 链接)。以下是一些奇怪的事情:
  • 如果在 main 中注释掉 constexpr,那么 MSVC 将生成错误的代码(即字符串的运行时转换),但 GCC 和 clang 都会生成正确的代码(即转换后的字符串常量嵌入到汇编中)。
  • 如果在 main 中没有注释掉 constexpr,那么 MSVC 将生成正确的代码(即转换后的字符串常量嵌入到汇编中),但 GCC 和 clang 都无法编译代码,并指出 t 没有被常量表达式初始化(请参见 godbolt)。最奇怪的是,GCC 的错误消息输出了转换后的字符串,并指出它不是常量表达式。
那么,按照 C++ 标准来说,哪个编译器是正确的?我应该向谁报告错误?向 GCC 和 Clang 的开发人员还是向 Microsoft?

4
MSVC 生成了错误的代码(即字符串的运行时转换)” 的意思是,MSVC 编译器生成的代码存在问题,会导致字符串在运行时进行转换。编译器在编译时不一定需要执行任何操作,除非必要。可以使用 constexpr 关键字来实现函数的编译期计算,但如果在需要常量表达式的上下文中调用该函数,则无法使用。 - Nicol Bolas
有趣的是,一旦你摆脱了Option基类(并通过函数调用根据需要生成string_view),代码在所有编译器上都可以正常工作。因此,在基类中可能会发生一些混淆编译器的事情。 - Nicol Bolas
@NicolBolas,我知道这是正确的代码。但对于我的目的来说是不正确的(即它不是我想要实现的)。 - DoDo
1个回答

8

constexpr声明在所有编译器中都可以使用,当t也被声明为静态时。

constexpr static auto t = make( "abcd_efgh_ijkl_mnop_peqst" );

原因在于 string_view。它是一个引用类型,指向正在初始化的对象。因此,你无论如何都在初始化一个 contexpr 指针。现在,一个(未初始化为 null 指针的)constexpr 指针只能用具有静态存储期的对象的地址来初始化。

[expr.const](强调是我的)

11 常量表达式是指一个glvalue核心常量表达式,它引用的实体是常量表达式的允许结果(如下所定义),或者是一个prvalue核心常量表达式,其值满足以下约束:

  • 如果该值是类类型的对象,则每个非静态数据成员的引用类型都引用了一个常量表达式的允许结果的实体。
  • 如果该值是指针类型,则它包含具有静态存储期的对象的地址,这种对象的末尾之后的地址([expr.add]),非立即函数的地址或空指针值
  • 如果该值是指向成员函数的指针类型,则它不指代立即函数。
  • 如果该值是类或数组类型的对象,则每个子对象都满足该值的这些约束。

如果一个实体是具有静态存储期的对象,并且既不是临时对象,也是一个临时对象,其值满足上述约束,或者如果它是一个非立即函数,则该实体是常量表达式的允许结果。

当您将对象声明为自动存储持续时间时,string_view中的指针未使用静态对象的地址进行初始化。因此,GCC和Clang会正确地发出警告。
自我引用是使此问题有趣且棘手的原因。

具体规则是:http://eel.is/c++draft/expr.const#11 - Barry
谢谢!所以,我猜这是MSVC的一个bug - 它也应该报告一个错误?另外,如果GCC和clang打印一个更有用的错误信息(例如引用你在这里引用的规则),那就太好了。 - DoDo
@DoDo - 是的,MSVC 明显是错了,接受自动 constexpr 变量。至于 Clang 和 GCC 的 QoI,可能有一些选项可以让它们打印出在常量求值中遇到问题的位置。但我并没有在脑海中记起来。 - StoryTeller - Unslander Monica
1
@DoDo Clang实际上告诉你问题(“'t'的子对象指针不是常量表达式”),尽管如果它也给出调用堆栈就更好了。已向gcc提交[96557](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=96557)。 - Barry

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