4

Consider the following program (sorry about the length; this is this shortest way I could think of to express the problem):

#include <iostream>
#include <vector>
#include <typeindex>

using namespace std;

std::vector<std::type_index>&
test_vector()
{
  static std::vector<std::type_index> rv;
  return rv;
}

template <typename T>
class RegistrarWrapper;

template<typename T>
class Registrar
{
  Registrar()
  {
    auto& test_vect = test_vector();
    test_vect.push_back(std::type_index(typeid(T)));
  }
  friend class RegistrarWrapper<T>;
};

template <typename T>
class RegistrarWrapper
{
  public:
    static Registrar<T> registrar;
    typedef Registrar<T> registrar_t;
};

template <typename T>
Registrar<T> RegistrarWrapper<T>::registrar;


template <typename T>
class Foo
{
  public:
    // Refer to the static registrar somewhere to make the compiler
    // generate it ?!?!?!?
    static constexpr typename RegistrarWrapper<Foo<T>>::registrar_t& __reg_ptr =
      RegistrarWrapper<Foo<T>>::registrar;
};


int main(int argc, char** argv)
{
  Foo<int> a;
  Foo<bool> b;
  Foo<std::string> c;

  for(auto&& data : test_vector()) {
    std::cout << data.name() << std::endl;
  }

}

When compiled with clang++ (version 3.5.2, of course with -std=c++11), this program outputs (piped through c++filt for readability):

Foo<int>
Foo<bool>
Foo<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > >

But with g++ (tried versions 4.8.5, 4.9.3, and 5.2.0), it outputs nothing! What's going on here? Which compiler is conforming to the c++ standard? How can I create this effect in a compiler-agnostic way (preferably without any run-time overhead)?

Daisy Sophia Hollman
  • 6,046
  • 6
  • 24
  • 35
  • …and you really couldn't simplify the test case any further? – Columbo Nov 03 '15 at 16:56
  • I've ran your code using clang++ 3.7.0 and it compiles cleanly, except for warning about unused variables – Amadeus Nov 03 '15 at 16:57
  • @Amadeus It compiles cleanly in both clang++ and g++. The problem is that the output is different. – Daisy Sophia Hollman Nov 03 '15 at 17:13
  • @Columbo I'm open to suggestions – Daisy Sophia Hollman Nov 03 '15 at 17:13
  • 1
    FWIW, it compiles and runs with VS2015 (even if the std::string type displays slightly different). – Bo Persson Nov 03 '15 at 17:44
  • @BoPersson Thanks. I don't have access to a lot of other compilers, and I'm interested to see if clang or gcc is the odd one out here. – Daisy Sophia Hollman Nov 03 '15 at 17:55
  • `Foo() { (void)&RegistrarWrapper>::registrar; }` (and getting rid of the `constexpr` member) solves it in all three compilers, and doesn't involve extra runtime overhead compared to your original solution. Some more ideas here: http://stackoverflow.com/a/27965993/4326278. – bogdan Nov 03 '15 at 19:05
  • @bogdan Of *course* the line about static data member initialization and usage is in the Templates clause... I spent like 30 minutes looking for it and gave up. Sigh. – Barry Nov 03 '15 at 19:17
  • @DavidHollman The answer you're looking for is a combination of the accepted answer on the linked question (how to do it) and the second answer (why gcc/clang give different answers) – Barry Nov 03 '15 at 19:28
  • @bogdan But this precludes, for instance, the ability to treat `Foo` as plain-old-data, since it now has a user-defined default constructor – Daisy Sophia Hollman Nov 03 '15 at 20:13
  • True, but you didn't mention that as a requirement in your question. I think requiring a trivial constructor makes the question different from the linked one. @Barry, what do you think? – bogdan Nov 03 '15 at 20:28
  • 1
    ... and in that case, I would go for `static char reg[register_class>()];`, where `register_class` is a `constexpr` function template that does the `(void)&RegistrarWrapper::registrar;` dance in its body and returns, say, `1`. – bogdan Nov 03 '15 at 20:40
  • @bogdan Yeah, that would work. Reopened. – Barry Nov 03 '15 at 21:03
  • @bogdan that works, thanks – Daisy Sophia Hollman Nov 03 '15 at 23:13

1 Answers1

1

First, a couple of solutions. For both of them, the essential part is taking the address of registrar from code that is guaranteed to be instantiated. This ensures that the definition of the static member is also instantiated, triggering the side effect.

The first one relies on the fact that the definition of the default constructor for each specialization of Foo is instantiated to handle the default initialization of a, b and c in main:

template<typename T> class Foo
{
public:
   Foo() { (void)&RegistrarWrapper<Foo<T>>::registrar; }
};

A disadvantage is that this introduces a non-trivial constructor. An alternative that avoids this issue is the following:

template<class T> constexpr std::size_t register_class() 
{ 
   (void)&RegistrarWrapper<T>::registrar; 
   return 1; 
}

template<typename T> class Foo
{
   static char reg[register_class<Foo<T>>()];
};

The key here is to trigger the instantiation in the declaration of the static member, without relying on any initializer (see below).

Both solutions work fine in Clang 3.7.0, GCC 5.2.0 and Visual C++ 2015, both with and without optimizations enabled. The second one uses the extended rules for constexpr functions, which are a C++14 feature. Of course, there are several easy ways to make it C++11 - compliant, if needed.


I think the problem with your solution is that there's no guarantee that the initializer for __reg_ptr gets instantiated if its value is not used somewhere. Some Standard quotes from N4527:

14.7.1p2:

[...] the initialization (and any associated side-effects) of a static data member does not occur unless the static data member is itself used in a way that requires the definition of the static data member to exist.

This doesn't quite address the constexpr case, since (I think) it's talking about the out-of-class definition of a static data member that is odr-used (it's more relevant to registrar), but it's close.

14.7.1p1:

[...] The implicit instantiation of a class template specialization causes the implicit instantiation of the declarations, but not of the definitions, default arguments, or exception-specifications of the class member functions, member classes, scoped member enumerations, static data members and member templates [...]

This guarantees that the second solution works. Note that it doesn't guarantee anything about an in-class initializer for a static data member.

There seems to be some uncertainty regarding instantiation of constexpr constructs. There's CWG 1581, which is not that relevant to our case, except that, at the very end, it talks about the fact that it's not clear whether constexpr instantiation happens during constant expression evaluation or during parsing. Some clarifications in this area may provide some guarantees for your solution as well (either way...) but we'll have to wait.


A third variant: a way to make your solution work is to explicitly instantiate the specializations of Foo, instead of relying on implicit instantiation:

template class Foo<int>;
template class Foo<bool>;
template class Foo<std::string>;

int main()
{
   for(auto&& data : test_vector()) {
      std::cout << data.name() << std::endl;
   }
}

This also works in all three compilers, and relies on 14.7.2p8:

An explicit instantiation that names a class template specialization is also an explicit instantiation of the same kind (declaration or definition) of each of its members [...]

Given that those are explicit instantiation definitions, this seems to be enough to convince GCC to instantiate the initializer for __reg_ptr. However, those explicit instantiation definitions can only appear once in the whole program ([14.7p5.1]), so extra care is needed. I consider the first two solutions to be more reliable.

bogdan
  • 9,229
  • 2
  • 33
  • 48