loading...
Cover image for C++ Template Specialization - Syntax Note

C++ Template Specialization - Syntax Note

deciduously profile image Ben Lovy ・3 min read

I sat down yesterday with @codemouse92 via Visual Studio Live Share for VS Code -- which is an awesome tool - to do a mostly straightforward re-arrangement of some C++. Unexpectedly, we ran into something, well...unexpected. To DEV!

The Concept

In C++, you can write a template:

template <typename T>
T myIdentityFunction(T val)
{
   return val;
}

This nearly useless function just returns whatever is passed in, no matter the type. You use it with a concrete type, like this:

#include <iostream>

int main()
{
    int someInt = 5;
    int aCopyOfTheSameInt = myIdentityFunction(someInt);
    std::cout << aCopyOfTheSameInt << "\n";
}

This will output 5, as expected:

$ clang++ test.cpp -o test
$ ./test 
5

When it gets used, the compiler will generate the specialized version and insert that in your binary:

int myIdentityFunction(int val)
{
    return val;
}

As I just learned today, you can specialize what types end up in your templates to retain control over what the compiler will guess:

template int myIdentityFunction(int val);

This is a silly example, but this ability lets you do things like partially specialize a template<typename T, typename U> to template<int, typename U>, and also makes implicit behaviour explicit, giving you back the keys from the compiler. You just define one for each type you need.

The Context

I don't actually know if the fact we're doing this in headers is relevant or not, but I'm including it for completeness in case somebody does know and wants to elaborate on this. I think the issue is just "in a class" vs. "not in a class".

We were refactoring a library to be header-only. In a standard library, you have your declarations in someModule.hpp:

class idksomefunctions
{
public:
    idksomefunctions() = delete; // specify there should be no constructor

    template <typename T>
    static T myIdentityFunction(T);  // Template declaration
};

And then a corresponding someModule.cpp with the actual implementations and specializations:

#include "someModule.hpp"

template <typename T>
T idksomefunctions::myIdentityFunction(T val)
{
    return val;
}

// Any specializations live here
template int idksomefunctions::myIdentityFunction(int val);

To refactor this into a header, you just combine them both in someModule.hpp:

class idksomefunctions
{
public:
    idksomefunctions() = delete; // specify there should be no constructor

    template <typename T>
    static T myIdentityFunction(T val)
    {
        return val;
    }

    template static int myIdentityFunction(int val);  // right??
};

Not quite:

$ clang++ test.cpp -o test
In file included from test.cpp:5:
./test.hpp:15:14: error: expected '<' after 'template'
    template static int myIdentityFunction(int val);  // right??
             ^
1 error generated.

Okay, try the other syntax:

-  template static int myIdentityFunction(int val);
+  template <> static int myIdentityFunction(int val);

Good to go!

The Switcheroo

Now, idksomefunctions doesn't really need to be a class - it's just, I don't know, some functions. This could just be a namespace. No more constructor thing, no more static or public (or storage class errors), just some good ol' functions:

- class idksomefunctions
+ namespace idksomefunctions
  {
-  public:
-     idksomefunctions() = delete; // specify there should be no constructor

      template <typename T>
-     static int myIdentityFunction(T val)
+     T myIdentityFunction(T val)
      {
          return val;
      }

      template <>
-     static int myIdentityFunction(int val);
+     int myIdentityFunction(int val);
  }

Great! But wait:

$ clang++ test.cpp -o test
/bin/x86_64-unknown-linux-gnu-ld: /tmp/test-d48bb0.o: in function `main':
test.cpp:(.text+0x13): undefined reference to `int idksomefunctions::myIdentityFunction<int>(int)'
clang-9: error: linker command failed with exit code 1 (use -v to see invocation)

That's no good. There's one more change to make:

      template <typename T>
      int myIdentityFunction(int val)
      {
          return val;
      }

-     template <>
+     template
      int myIdentityFunction(int val);

Gotta take out the <> thingy, back to where we started! Now it'll compile:


#include <iostream>

namespace idksomefunctions
{
    template <typename T>
    int myIdentityFunction(T val)
    {
        return val;
    }

    template int myIdentityFunction(int val);
};

int main()
{
    int someInt = 5;
    int aCopyOfTheSameInt = idksomefunctions::myIdentityFunction(someInt);
    std::cout << aCopyOfTheSameInt << "\n";
}

The Recap

Inside a class, you specialize via:

template<> static int myIdentityFunction(int val);

Outside of a class, though, you omit the thingy:

template int myIdentityFunction(int val);

The Question

What am I saying when I say template <> versus template here?

Photo by Ricardo Gomez Angel on Unsplash

Posted on by:

Discussion

markdown guide
 

Hello!

It works if you define it it outside class scope: godbolt.org/z/mL7JsO

I tried to find a reason in the standard for it to work like this but didn't find anything

 

Thanks for the insight, Roger. It's odd that this form of specialization, just limiting the valid argument types for a generic function, is not documented very well...or, really, at all as far as I can tell.

 

Hello Jason, I forgot to post two bits of documentation I saved in my first reply:

14.7.3/2:
An explicit specialization shall be declared in a namespace enclosing the specialized template. An explicit specialization whose declarator-id is not qualified shall be declared in the nearest enclosing namespace of the template, or, if the namespace is inline (7.3.1), any namespace from its enclosing namespace set. Such a declaration may also be a definition. If the declaration is not a definition, the specialization may be defined later (7.3.1.2).

7.1.1/1:
At most one storage-class-specifier shall appear in a given decl-specifier-seq, except that thread_local
may appear with static or extern. If thread_local appears in any declaration of a variable it shall be
present in all declarations of that entity. If a storage-class-specifier appears in a decl-specifier-seq, there
can be no typedef specifier in the same decl-specifier-seq and the init-declarator-list of the declaration shall
not be empty (except for an anonymous union declared in a named namespace or in the global namespace,
which shall be declared static (9.5)). The storage-class-specifier applies to the name declared by each
init-declarator in the list and not to any names declared by other specifiers. A storage-class-specifier shall
not be specified in an explicit specialization (14.7.3) or an explicit instantiation (14.7.2) directive.

This code

class idksomefunctions {
public:
    idksomefunctions() = delete; // specify there should be no constructor

    template <typename T>
    static T myIdentityFunction(T val)
    {
        return val;
    }

    template static int myIdentityFunction(int val);
};

looks ok according to the first bit, but if I got it correctly, it fails because of this rule from the second one "A storage-class-specifier shall not be specified in an explicit specialization"

Delicious spec quotes. :)

Makes total sense. I've always done this sort of specialization in the .cpp file, but the transition to header-only on this library has forced me to consider some of the deeper technical implications.

 

Huh, interesting. Thanks!