Barry
Barry

Reputation: 304172

Declaring global const objects in a header file

In Eric Niebler's range-v3 library, he provides a lot of headers that each have their own global function object. They are all declared in the same way. He provides a class template static_const:

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

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

And then every function object of type F is declared as:

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

What are the advantages of introducing the object through the static_const template and in an unnamed namespace, as opposed to just writing:

static constexpr F f{};

Upvotes: 10

Views: 561

Answers (1)

Barry
Barry

Reputation: 304172

The issue is basically the one definition rule.

If you just have:

static constexpr F f{};

The name f has internal linkage, which means that every translation unit has its own f. The consequence of that means that, for instance, an inline function which takes the address of f would get a different address based on which translation unit the call occurred in:

inline auto address() { return &f; } // which f??

Which means now we might actually have multiple definitions of address. Really, any operation that takes the address of f is suspect.

From 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
}

The code above demonstrates the potential for ODR violations if the global std::begin function object is defined naïvely. Both functions fun in file1.cpp and main in file2.cpp cause the implicit instantiation foo<int[4]>. Since global const objects have internal linkage, both translation units file1.cpp and file2.cpp see separate std::begin objects, and the two foo instantiations will see different addresses for the std::begin object. That is an ODR violation.


On the other hand, with:

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

while f still has internal linkage, static_const<F>::value has external linkage due to its being a static data member. When we take the address of f, it being a reference means that we're actually taking the address of static_const<F>::value, which only has one unique address across the whole program.

An alternative is to use variable templates, which are required to have external linkage - which requires C++14, and is also demonstrated in that same link:

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

Because of the external linkage of variable templates, every translation unit will see the same address for __static_const<__detail::__begin_fn>. Since std::begin is a reference to the variable template, it too will have the same address in all translation units.

The anonymous namespace is needed to keep the std::begin reference itself from being multiply defined. So the reference has internal linkage, but the references all refer to the same object. Since every mention of std::begin in all translation units refer to the same entity, there is no ODR violation ([basic.def.odr]/6).


In C++17, we won't have to worry about this at all with the new inline variable feature, and just write:

inline constexpr F f{};

Upvotes: 2

Related Questions