为什么外部链接变量可以用作常量表达式?

15
在另一个问题的讨论中,我被给出了一个例子,其中标识符的关联性显然影响了它在常量表达式中的可用性:
extern char const a[] = "Alpha";
char constexpr b[] = "Beta";
char const g[] = "Gamma";

template <const char *> void foo() {}

auto main()
    -> int
{
    foo<a>();     // Compiles
    foo<b>();     // Compiles
    foo<g>();     // Doesn't compile
}

上一个错误(使用GCC)的内容为:
test.cc: In function 'int main()':
test.cc:12:13: error: the value of 'g' is not usable in a constant expression
         foo<g>();     // Doesn't compile
             ^
test.cc:3:16: note: 'g' was not declared 'constexpr'
     char const g[] = "Gamma";
                ^

在早期的讨论中,我可能错过了示例的重要性,因为我认为仅仅是链接使得foo<a>foo<g>不同 - 但是,我开始怀疑这个观点。

  1. 真的是链接吗,还是extern提供的其他属性使foo<a>()成为可能?
  2. 允许foo<a>()而不允许foo<g>()的原理是什么?特别是,如果它是由链接决定的,那么为什么内部链接会导致变量不能用作常量表达式,而相同的变量声明为extern则可以使用?
  3. 有人认为符号是否对链接器可见与此相关。对我来说,当添加static时仍允许foo<b>变体的事实证明了这一点 - 或者我错了吗?
  4. (foo<b>()foo<g>()之间的区别已经被其他问题充分解释了,我想)。

1
Clang 愉快地接受这三个。 - T.C.
@T.C. 嗯,是这样的...该死。你知道哪个编译器是正确的吗? - davmac
GCC 6也接受-std=c++1z - T.C.
@T.C. 这意味着GCC的人认为它在2014年之后发生了变化,但LLVM的人则认为是自2011年以来? - davmac
肯定是GCC的bug。 N3337 [temp.arg.nontype]有一个直接相关的例子。 - T.C.
2个回答

6

GCC错误。

N3337(即C++11 + 编辑修正)[temp.arg.nontype]/2提供了一个例子,直接说明了问题:

template<class T, const char* p> class X {
    /* ... */
};
X<int, "Studebaker"> x1; // error: string literal as template-argument

const char p[] = "Vivisectionist";
X<int,p> x2; // OK

在C++03中,引用/指针模板参数仅限于具有外部链接的对象,但是这个限制在C++11中被删除了。

在C++17中,引用/指针模板参数的规则放宽,允许所有常量表达式,因此GCC接受带有-std=c++1z示例的原因可能是因为它在该模式下通过了不同的代码路径。


帽子。真的很有趣。+1 - skypjack

3

这真是个巧合。昨晚我刚在C++ Templates中读到了相关内容。当把指针作为模板非类型参数时,编译器会将指针中存储的地址而不是所指向的作为常量替换模板参数。因此,为避免ODR(One Definition Rule)问题,该地址必须在编译期间可知且在所有编译单元中唯一。对于constexprextern变量是如此,但对于具有文件或全局链接的变量则不是。以下是一个例子。

static char const foo[] = "Hello";
char const bar[] = "Hello";
constexpr char const baz[] = "Hello";
extern char const qux[] = "Hello";

template <char const*>
struct my_struct{};

int main() {
    my_struct<foo> f;       // ERROR: Address is unique, but not known until runtime
    my_struct<bar> b;       // ERROR: Address may or may not be unique (ODR violation) and not known until runtime
    my_struct<baz> bz;      // OK: constexpr
    my_struct<qux> q;       // OK: extern
}

这并不是真的,因为C++11已经移除了链接限制。 - T.C.
1
在所有四个示例中,链接器决定变量的地址。编译器永远不知道将混合在一起以创建最终可执行文件的其他编译单元是什么。 - brian beuning
@T.C. 我已经使用 -std=c++14 选项测试了我的代码,但是看起来 gcc 在这个领域有一个 bug。我应该使用第二个编译器。我知道 C++17 将支持将 nullptr 作为非类型参数,但我不知道在 C++11 中链接规则已经发生了如此大的变化。 - Tim
@brianbeuning 要非常严谨,加载器决定地址。 :D 实际上,链接器将根据编译器生成的各个对象的布局确定可执行文件中的偏移量,当它将各个部分融合在一起时。我是说,在这些对象中,编译器将在实例化时“知道”地址。不过,我不知道编译器在实例化时如何区分 foobar。这确实是一个实现细节,但我很想了解。 - Tim
2
如果在多个翻译单元中具有外部链接的情况下定义了多个 bar,那么您将会遇到ODR冲突,在这种情况下,链接器将只选择一个(贪婪解决),或者会以错误退出(标准不要求该行为),或者进行其他的操作(也许是某种启发式方法)。 - Tim
@Tim 我认为关于 bar[] 的关键是可能有其他编译单元将其定义为 char const bar[] = "World!"; 而编译器不会知道。并非所有链接器都会抱怨。对于严谨的观点,使用 -fpic 编译的代码必须由加载程序决定加载位置,但未使用 -fpic 编译的代码必须在链接器指定的位置加载。 - brian beuning

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