Can a template specialization affect a variable it does not specialize?

Viewed 89

I have a c++ class template which contains a couple of static variables, which I wish to specialize for various different values in an enum. Each specialization is declared in its own header file, and defined in its own source file (side note -- is this problematic? Would it be better to declare all the specializations in the template's header file, and define them all in the associated source file?)

I have two strings, defaultName and name, that I'd like to initialize to the same value as each other for each specialization, and for the unspecialized case. In the unspecialized case, I give defaultName its default value, and assign name the value of defaultName: template<Spec S> std::string MyClass<S>::name = defaultName;. The only way I can see of doing the same for all is to duplicate this initialization in each of my specializations (e.g. template<> std::string MyClass<A>::name = defaultName;). It seems to me that it would be more convenient if there were a way of applying the unspecialized case template<Spec S> std::string MyClass<S>::name = defaultName; in the absence of a more specific initialization for name.

Stripping out the unrelated code and neglecting include guards, here's what it looks like. Specialization for A produces the unexpected behaviour, while specialization for B (as well as the unspecialized C) produces the expected behaviour.

/* MyClass.hh */

enum Spec{
    A,
    B,
    C
};

template <Spec S> class MyClass{
    private:
        const static std::string defaultName;
        static std::string name;
        
    public:
        MyClass<S>() {};
        
        friend std::ostream& operator<< (std::ostream& out, const MyClass<S>& obj){
            return out << obj.defaultName << ": " << obj.name;
        }
        
        static void changeName(const std::string& newName){
            name = newName;
        }
};

template<Spec S> const std::string MyClass<S>::defaultName = "default";

template<Spec S> std::string MyClass<S>::name = defaultName;
/* A.hh -- first specialization */
#include "MyClass.hh"
#include <string>

template<> const std::string MyClass<A>::defaultName;
/* A.cc -- first specialization, definition */
#include "A.hh"

template<> const std::string MyClass<A>::defaultName = "A";
/* B.hh -- second specialization */
#include "MyClass.hh"
#include <string>

template<> const std::string MyClass<B>::defaultName;

template<> std::string MyClass<B>::name; //adding this produces expected behaviour
/* B.cc -- second specialization, definition */
#include "B.hh"

template<> const std::string MyClass<B>::defaultName = "B";

template<> std::string MyClass<B>::name = defaultName; //adding this produces expected behaviour
/* main.cc */

#include <iostream>
#include <string>

#include "MyClass.hh"
#include "A.hh"
#include "B.hh"

int main() {
    std::cout << MyClass<A>() << std::endl;
    std::cout << MyClass<B>() << std::endl;
    std::cout << MyClass<C>() << std::endl;
    
    MyClass<A>::changeName("A2");
    MyClass<B>::changeName("B2");
    MyClass<C>::changeName("C2");
    
    std::cout << MyClass<A>() << std::endl;
    std::cout << MyClass<B>() << std::endl;
    std::cout << MyClass<C>() << std::endl;
}

What I would expect to happen in specialization A is to initialize defaultName to "A" using the explicit specialization, then fall back to the template for all remaining initializations, including initializing name from defaultName. That would produce output like this:

A: A
B: B
default : default
A: A2
B: B2
default : C2

Instead, what I observe is that name for specialization A behaves as though it is an empty string, which I believe means it is uninitialized for my compiler (gcc 9.3.0):

A: 
B: B
default : default
A: A2
B: B2
default : C2

This strikes me as strange, because not only is name not being initialized from defaultName in the specialization A, but it's also not being initialized from defaultName in the unspecialized case. What's going on here? Is it just that the value of defaultName to use is ambiguous, and so rather than make a decision, no value is selected?

Interestingly, I no longer have the same problem if I move the contents of MyClass.hh, A.cc and B.cc into main.cc, the output is what I would expect. However, this solution won't work for larger programs, as including the definitions of MyClass<A>::defaultName in the header MyClass.hh leads to multiple definition linker errors, and moving the definition into a new associated source file MyClass.cc returns to the same behaviour I'm observing now.

Any thoughts on what's going on here would be much appreciated!

0 Answers
Related