Search code examples
c++c++11rvalue-referencecompiler-bug

Compiler error or correct behavior for static const member variable, variadic templates, and &&?


I have noticed a strange behavior when trying to compile the code included below. I have 4 files as follows

createshared.h:

#ifndef CREATESHARED_H_
#define CREATESHARED_H_

#include <memory>
#include <utility>

#ifdef USE_REFREF
template<typename T, typename... Args>
std::shared_ptr<T> create_shared(Args&&... args)
{
    class HelperClass : public T
    {
    public:
        HelperClass (Args&& ... nargs) : T(std::forward<Args...>(nargs)...) {}
        virtual ~HelperClass() = default;
    };

    return std::make_shared<HelperClass>(std::forward<Args...>(args)...);
}
#else
template<typename T, typename... Args>
std::shared_ptr<T> create_shared(Args... args)
{
    class HelperClass : public T
    {
    public:
        HelperClass (Args ... nargs) : T(nargs...) {}
        virtual ~HelperClass() = default;
    };

    return std::make_shared<HelperClass>(args...);
}
#endif

#endif

staticinitclass.h

#ifndef STATICINITCLASS_H_
#define STATICINITCLASS_H_

class StaticInitClass
{
public:
#ifdef INITIALIZE_IN_HEADER
    static const int default_i = 1;
#else
    static const int default_i;
#endif
    virtual ~StaticInitClass() = default;
    StaticInitClass() = delete;
protected:
    StaticInitClass(int i);
};

#endif

staticinitclass.cpp:

#include "staticinitclass.h"

#include <iostream>

#ifndef INITIALIZE_IN_HEADER
const int StaticInitClass::default_i = 2;
#endif

StaticInitClass::StaticInitClass(int i)
{
    std::cout << "Created with " << i << std::endl;
}

main.cpp:

#include "staticinitclass.h"
#include "createshared.h"
#include <memory>

int main(int argc, const char* argv[])
{
    auto shared = create_shared<StaticInitClass>(StaticInitClass::default_i);
}

With no flags, the program compiles and runs fine.

$ g++ -std=c++11 main.cpp staticinitclass.cpp 
$ ./a.out 
Created with 2

Fine, because default_i is an integral type, we can initialize it in the header. Let's do that

$ g++ -std=c++11 main.cpp staticinitclass.cpp -DINITIALIZE_IN_HEADER
$ ./a.out 
Created with 1

Good, still compiles and works fine. Now, let's add our && and std::forward

$ g++ -std=c++11 main.cpp staticinitclass.cpp -DINITIALIZE_IN_HEADER -DUSE_REFREF
/tmp/cc3G4tjc.o: In function `main':
main.cpp:(.text+0xaf): undefined reference to `StaticInitClass::default_i'
collect2: error: ld returned 1 exit status

Linker error. Well, let's now try initializing our default_i member in the .cpp

$ g++ -std=c++11 main.cpp staticinitclass.cpp -DUSE_REFREF
$ ./a.out 
Created with 2

And it works again. Using clang yields the same result, which would lead me to believe that this isn't just an isolated compiler error, but perhaps something in the language itself that prevents the static initialization. I just can't seem to connect why adding && would cause the break.

Currently I am using g++ 4.8.2 and clang++ 3.5 on Ubuntu 14.04

Any ideas what is broken here when using -DINITIALIZE_IN_HEADER and -DUSE_REFREF?


Solution

  • Following §9.4.2 [class.static.data]:

    3 If a non-volatile const static data member is of integral or enumeration type, its declaration in the class definition can specify a brace-or-equal-initializer in which every initializer-clause that is an assignment expression is a constant expression (5.19). [...] The member shall still be defined in a namespace scope if it is odr-used (3.2) in the program and the namespace scope definition shall not contain an initializer.

    In other words, giving a const static data member a value directly in a header does not mean you don't need to define that data member. You should have this in staticinitclass.cpp file:

    #ifndef INITIALIZE_IN_HEADER
    const int StaticInitClass::default_i = 2;
    #else
    const int StaticInitClass::default_i; // this is what you don't have
    #endif
    

    Binding to a reference (to a forwarding reference && in your case deduced as const lvalue reference) counts as odr-use of this data member.

    In case you don't use a forwarding reference and you take the argument by-value, then it is not an odr-use of that static data member, therefore no linker error is raised.