【问题标题】:Declaring global const objects in a header file在头文件中声明全局 const 对象
【发布时间】:2016-06-27 18:39:59
【问题描述】:

在 Eric Niebler 的 range-v3 库中,他提供了很多头文件,每个头文件都有自己的全局函数对象。它们都以相同的方式声明。他提供了一个类模板static_const

template<typename T>
struct static_const
{
    static constexpr T value {};
};

template<typename T>
constexpr T static_const<T>::value;

然后每个F类型的函数对象被声明为:

namespace
{
    constexpr auto&& f = static_const<F>::value;
}

通过static_const模板在未命名的命名空间中引入对象有什么好处,而不是只写:

static constexpr F f{};

【问题讨论】:

  • conexpr 在这里非常重要。如果它是一个简单的const 或一个非常量并且你要使用这样的模板,你不能保证一个接一个的初始化,因为有序的初始化对于实例化的变量,不会在 TU 内完成。 constexpr 保证不会发生动态初始化,所以这在此处无法观察到,AFAIK。
  • static_const 的目的地在 Eric Niebler 的D4381 和对应的article 中有详细描述。主要涉及one-definition-rule 并防止其违反。
  • 这基本上是穷人的内联变量。
  • @Barry 参见N4424,一个提议引入内联变量主要是为了解决 ODR 问题。
  • @Orient,这应该是一个答案(因为它是正确的)。

标签: c++ global-variables c++14 range-v3


【解决方案1】:

问题基本上是一个定义规则。

如果你有:

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&lt;int[4]&gt;。由于全局 const 对象具有内部链接,因此翻译单元 file1.cpp 和 file2.cpp 都会看到单独的 std::begin 对象,并且两个 foo 实例化将看到 std::begin 对象的不同地址。这违反了 ODR。


另一方面,与:

namespace
{
    constexpr auto&& f = static_const<F>::value;
}

虽然f 仍具有内部链接,但static_const&lt;F&gt;::value 具有外部 链接,因为它是静态数据成员。当我们取f的地址时,它是一个引用意味着我们实际上取的是static_const&lt;F&gt;::value的地址,它在整个程序中只有一个唯一的地址。

另一种方法是使用需要具有外部链接的变量模板 - 这需要 C++14,并且在同一链接中也有演示:

namespace std {
  template <class T>
  constexpr T __static_const{};
  namespace {
    constexpr auto const& begin =
      __static_const<__detail::__begin_fn>;
  }
}

由于变量模板的外部链接,每个翻译单元将看到__static_const&lt;__detail::__begin_fn&gt; 的相同地址。由于std::begin 是对变量模板的引用,它在所有翻译单元中也将具有相同的地址。

需要匿名命名空间来防止std::begin 引用本身被多次定义。所以引用具有内部链接,但引用都引用同一个对象。由于在所有翻译单元中每次提到 std::begin 都指的是同一个实体,因此不存在 ODR 违规 ([basic.def.odr]/6)。


在 C++17 中,有了新的内联变量功能,我们根本不必担心这个问题,只需编写:

inline constexpr F f{};

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-04-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-08-25
    • 1970-01-01
    相关资源
    最近更新 更多