0
votes

I have not kept up with the intricacies of templates so just know enough to be dangerous. However, I ran to a problem (reported by one of our testers) where code compiled properly on Xcode (Clang) and Visual Studio 2019 but the behavior was different and I'm wondering which one was "correct"

Consider the following simple class:

template <class T>
class foo
{
   void set(int index, T value);
};

That default templated set method worked fine for most values of type T but I needed to specialize the behavior for a particular type, call it String.

So in the associated CPP file, I added the following:

template <>
void foo<String>::set(int index,  String value)
{
   // code here
};

Under Xcode, this worked perfectly fine. A call to set with a second parameter of String correctly invoked that specialized template.

However, the exact same code, compiled with Visual Studio failed, behaved differently. That same call with the String parameter just invoked the original set rather than than the specialized version.

I'm assuming that under Xcode, the compiler properly created a call with a signature that matched the specialized version that was available in the CPP file and hence was linked properly.

So my questions are:

  1. Why did this work in Xcode but not in Visual Studio
  2. Which compiler behaved correctly and should the issue be considered a bug in the other compiler?

Incidentally, I did try creating an explicit signature in the header file:

template<>
void foo<String>::set(int index, String value );

and Xcode didn't complain about it and everything still worked. However, Visual Studio, while not complaining about that header signature, did complain at link time that it couldn't find a matching implementation and so:

  1. Why did that not work?

While I did find a way to do it that worked for both compilers (essentially putting inline versions in the header file, I would really like to understand why this problem happened.

I thank people in advance.

Edit: bllow is a real example I just put together. This compiles and runs perfectly fine under Xcode (in particular, the string specialization is correctly called for variable g) but fails to link in Visual Studio 2019 with the following error

( public: void __cdecl Foo<class std::basic_string<char,struct std::char_traits,class std::allocator > >::print(int,class std::basic_string<char,struct std::char_traits,class std::allocator >)" (?print@?$Foo@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@@QEAAXHV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z) referenced in function main) referenced in function main )

    Classes.h
    ---------
    #pragma once

    #include <iostream>
    #include <string>

    template <class T>
    class Foo
    {
       public:
          void print(int index, T value)
             {
                std::cout << "Using implicit template\n";
             }
    };




    Classes.cpp
    -----------

    #include "Classes.h"


    template<>
    void Foo<std::string>::print(int i, std::string xyz)
    {
       std::cout << "Using explicit String template\n";
    }



    Main.cpp
    --------

    #include <string>
    #include <iostream>

    #include "Sub/Classes.h"

    //==============================================================================
    int main (int argc, char* argv[])
    {

        Foo<double> f;
        
        f.print(1, 2.0);
        
        Foo<std::string> g;
        
        g.print(1, "abc");

        return 0;
    }
1
Don't you mean T value and String value?Happy Green Kid Naps
Please show real code that demonstrates your compilation failure, that can be cut/pasted exactly as shown and then it can be tried to compile with various compilers. The shown code is obviously not the real code, and it cannot be determined which compiler produces correct results, without actual code that exhibits diverging behavior from different compilers.Sam Varshavchik
Without knowing how the code that calls foo<String>::set() sets things up (e.g. does it have visibility of the definitions of both foo and String) it's impossible to say. Reading up on providing a minimal reproducible example.Peter
@HappyGreenKidNaps Yes, sorry. As it happens, the code is actually part of a compiler for a special purpose language in which variable declarations are more like Pascal than C and sometimes I mix them up. I've edited the question to correct that error and thanks for pointing it out.David
@SamVarshavchik I have created a tiny example and edited my original post to include the three files needed to demonstrate the behavior.David

1 Answers

0
votes

You need to forward declare the full specialization in the .h file to tell other translation units not to instantiate the general template.


template <typename T> struct foo {
  void set(int, T);
};

template <>
void foo<String>::set(int, String);

If you don't do this, you're violating the One Definition Rule. This happens because, when compiling other .cpp files, the compiler doesn't know that a full specialization exists, and instantiates the general template.

Say in bar.cpp you write foo<String>().set(1, {});. The compiler constructs that function from the definition of the general template and emits it in the .o or .obj file. At link time, the linker sees multiple versions of foo<String>::set. It assumes they are equivalent and picks one at random. You can't know in general which it will pick. What's worse is that some calls to set could have been inlined, and the version called there could be different from the one the linker picked. This is why ODR violations are so dangerous.

Putting the forward declaration template <> void foo<String>::set(int, String); in the header tells all translation units "don't automatically instantiate this template, I promise to put this definition in some .cpp file." Now bar.cpp doesn't instantiate the template. It calls foo<String>::set as it would any other forward-declared function. foo.cpp provides the definition for that full specialization, and it is used everywhere.