问题基本上是一个定义规则。
如果你有:
static constexpr F f{};
名称f 具有内部链接,这意味着每个翻译单元都有自己的f。这样做的结果意味着,例如,一个采用f 地址的内联函数将根据调用发生在哪个翻译单元中获得不同的地址:
inline auto address() { return &f; } // which f??
这意味着现在我们实际上可能有多个address 的定义。真的,任何取f 地址的操作都是可疑的。
来自D4381:
// <iterator>
namespace std {
// ... define __detail::__begin_fn as before...
constexpr __detail::_begin_fn {};
}
// header.h
#include <iterator>
template <class RangeLike>
void foo( RangeLike & rng ) {
auto * pbegin = &std::begin; // ODR violation here
auto it = (*pbegin)(rng);
}
// file1.cpp
#include "header.h"
void fun() {
int rgi[] = {1,2,3,4};
foo(rgi); // INSTANTIATION 1
}
// file2.cpp
#include "header.h"
int main() {
int rgi[] = {1,2,3,4};
foo(rgi); // INSTANTIATION 2
}
上面的代码演示了如果全局 std::begin 函数对象被天真地定义,则可能发生 ODR 违规。 file1.cpp 中的 fun 函数和 file2.cpp 中的 main 函数都会导致隐式实例化 foo<int[4]>。由于全局 const 对象具有内部链接,因此翻译单元 file1.cpp 和 file2.cpp 都会看到单独的 std::begin 对象,并且两个 foo 实例化将看到 std::begin 对象的不同地址。这违反了 ODR。
另一方面,与:
namespace
{
constexpr auto&& f = static_const<F>::value;
}
虽然f 仍具有内部链接,但static_const<F>::value 具有外部 链接,因为它是静态数据成员。当我们取f的地址时,它是一个引用意味着我们实际上取的是static_const<F>::value的地址,它在整个程序中只有一个唯一的地址。
另一种方法是使用需要具有外部链接的变量模板 - 这需要 C++14,并且在同一链接中也有演示:
namespace std {
template <class T>
constexpr T __static_const{};
namespace {
constexpr auto const& begin =
__static_const<__detail::__begin_fn>;
}
}
由于变量模板的外部链接,每个翻译单元将看到__static_const<__detail::__begin_fn> 的相同地址。由于std::begin 是对变量模板的引用,它在所有翻译单元中也将具有相同的地址。
需要匿名命名空间来防止std::begin 引用本身被多次定义。所以引用具有内部链接,但引用都引用同一个对象。由于在所有翻译单元中每次提到 std::begin 都指的是同一个实体,因此不存在 ODR 违规 ([basic.def.odr]/6)。
在 C++17 中,有了新的内联变量功能,我们根本不必担心这个问题,只需编写:
inline constexpr F f{};