What Are Class Templates with Member Functions?

A class template is a pattern the compiler stamps out once per type you use it with. When that pattern contains member functions, those functions are stamped out too, and the class template's type parameters are available inside every one of them.

Everything in this lesson comes down to a single question: where does the template parameter declaration have to appear? Put a member function inside the class body and the answer is "nowhere, the class already declared it". Move that same function below the class and you suddenly owe the compiler two extra pieces of syntax. Learn which two, and class templates with member functions stop being fiddly.

The running example is a weaver's sett, the thread density of a piece of cloth. ends are the lengthwise warp threads and picks are the crosswise weft threads. Both are counted per unit of width, in whole threads per inch on an imperial loom or fractional threads per centimetre on a metric one, which is exactly the sort of "same idea, two numeric types" situation a class template is for.

Where the Type Parameter Is Visible

template <typename T> written above a class puts T in scope for the entire class body. That includes four distinct places, all shown below:

#include <iostream>

template <typename T>
class Sett
{
private:
    T m_ends{};  // T as the type of a data member
    T m_picks{};

public:
    Sett(const T& ends, const T& picks)  // T as the type of a parameter
        : m_ends{ ends }
        , m_picks{ picks }
    {
    }

    T threadCount() const  // T as a return type
    {
        T total{ m_ends + m_picks };  // T as the type of a local variable
        return total;
    }

    bool fitsReed(const T& dents) const
    {
        return m_ends <= dents;
    }

    void describe() const
    {
        std::cout << m_ends << " ends / " << m_picks << " picks" << '\n';
    }
};

int main()
{
    Sett<int> tabby{ 24, 22 };
    tabby.describe();
    std::cout << "total per inch: " << tabby.threadCount() << '\n';
    std::cout << "fits a 30 dent reed: " << (tabby.fitsReed(30) ? "yes" : "no") << '\n';

    Sett<double> metric{ 9.5, 8.25 };
    metric.describe();
    std::cout << "total per centimetre: " << metric.threadCount() << '\n';

    return 0;
}
24 ends / 22 picks
total per inch: 46
fits a 30 dent reed: yes
9.5 ends / 8.25 picks
total per centimetre: 17.75

Not one of those member functions carries a template line of its own. They inherit the class template's parameter declaration simply by being written inside the class body.

Two things about the class are worth pausing on.

The data members are private, so Sett is not an aggregate and cannot be initialized with aggregate initialization. A constructor does the job instead, and that constructor takes its parameters as const T&. T could turn out to be a type that is expensive to copy, and a reference to const costs nothing extra when it turns out to be int.

Best Practice
Take class template constructor parameters by const T&. You do not know what T will be, so assume copying it is expensive.

Splitting the Definition Out of the Class Body

Long member functions clutter a class definition, so you will often want to declare a member function inside the class and define it below. Doing that costs two additions:

  1. A fresh template parameter declaration, because the class template's one does not reach outside the class body.
  2. The fully templated name as the qualifier: Sett<T>::, not Sett::.

Here is the same class with both member functions moved out:

#include <iostream>

template <typename T>
class Sett
{
private:
    T m_ends{};
    T m_picks{};

public:
    Sett(const T& ends, const T& picks)
        : m_ends{ ends }
        , m_picks{ picks }
    {
    }

    T threadCount() const;
    void describe() const;
};

template <typename T>
T Sett<T>::threadCount() const
{
    return m_ends + m_picks;
}

template <typename T>
void Sett<T>::describe() const
{
    std::cout << m_ends << " ends / " << m_picks << " picks" << '\n';
}

int main()
{
    Sett tabby{ 24, 22 };
    tabby.describe();
    std::cout << "total per inch: " << tabby.threadCount() << '\n';

    return 0;
}
24 ends / 22 picks
total per inch: 46

The decision table is short:

Member function is defined Needs template <typename T>? Needs Sett<T>:: qualifier?
Inside the class body No No
Below the class body Yes Yes

Notice Sett tabby{ 24, 22 }; in main(). No template arguments, no deduction guide, and it still compiles. Class template argument deduction works out of the box here because the constructor takes two const T& parameters, which is all the compiler needs to conclude that T is int. Deduction guides are only needed when no constructor supplies that information, as with an aggregate.

Two Mistakes the Compiler Will Catch

Both additions are mandatory, and skipping either produces a diagnostic worth recognising on sight.

Broken: the template parameter declaration is missing, so T is just an undeclared name at file scope.

#include <iostream>

template <typename T>
class Sett
{
private:
    T m_ends{};
    T m_picks{};

public:
    Sett(const T& ends, const T& picks)
        : m_ends{ ends }
        , m_picks{ picks }
    {
    }

    T threadCount() const;
};

// forgot the template parameter declaration
T Sett<T>::threadCount() const
{
    return m_ends + m_picks;
}

int main()
{
    Sett tabby{ 24, 22 };
    std::cout << tabby.threadCount() << '\n';

    return 0;
}
s.cpp:21:1: error: 'T' does not name a type
   21 | T Sett<T>::threadCount() const
      | ^

Broken: the qualifier drops the <T>, so the compiler does not believe this is a member of anything.

#include <iostream>

template <typename T>
class Sett
{
private:
    T m_ends{};
    T m_picks{};

public:
    Sett(const T& ends, const T& picks)
        : m_ends{ ends }
        , m_picks{ picks }
    {
    }

    T threadCount() const;
};

// forgot to qualify with the fully templated name
template <typename T>
T Sett::threadCount() const
{
    return m_ends + m_picks;
}

int main()
{
    Sett tabby{ 24, 22 };
    std::cout << tabby.threadCount() << '\n';

    return 0;
}

This one cascades, because once the function is not a member the body cannot see the data members either. Only the first two errors are shown here; two more follow complaining that m_ends and m_picks are undeclared.

s.cpp:22:3: error: 'template<class T> class Sett' used without template arguments
   22 | T Sett::threadCount() const
      |   ^~~~
s.cpp:22:23: error: non-member function 'T threadCount()' cannot have cv-qualifier
   22 | T Sett::threadCount() const
      |                       ^~~~~

The second error is the giveaway: the compiler read Sett::threadCount as a plain function, and plain functions cannot be const.

The Injected Class Name

Look back at the constructor. A constructor's name must match its class name, the class is Sett<T>, and yet the constructor is written Sett. Why is that not an error?

Every class quietly declares one extra name inside itself: its own unqualified name, known as the injected class name. For a class template, that injected name arrives with the template arguments already attached, so the bare token Sett written anywhere in Sett<T>'s scope expands to Sett<T>. The constructor's name does match after all.

The shorthand is not restricted to constructors. It works anywhere inside the class scope, including parameter types, and the scope of a member function defined below the class counts as inside:

#include <iostream>

template <typename T>
class Sett
{
private:
    T m_ends{};
    T m_picks{};

public:
    Sett(const T& ends, const T& picks)
        : m_ends{ ends }
        , m_picks{ picks }
    {
    }

    bool denserThan(const Sett& other) const;
};

template <typename T>
bool Sett<T>::denserThan(const Sett& other) const
{
    return (m_ends + m_picks) > (other.m_ends + other.m_picks);
}

int main()
{
    Sett tabby{ 24, 22 };
    Sett twill{ 30, 28 };

    std::cout << "tabby beside twill: " << (tabby.denserThan(twill) ? "denser" : "looser") << '\n';
    std::cout << "twill beside tabby: " << (twill.denserThan(tabby) ? "denser" : "looser") << '\n';

    return 0;
}
tabby beside twill: looser
twill beside tabby: denser

const Sett& other appears twice, once in the declaration and once in the definition, and in both places it means const Sett<T>&. Writing it out in full is equally correct and slightly noisier.

Key Concept
Class template argument deduction does not apply to function parameters, since it deduces from arguments rather than from parameters. An injected class name in a parameter list is not deduction at all: it is a name that already stands for Sett<T> before deduction is ever considered.

More Than One Type Parameter

Nothing changes in principle when a class template has several type parameters, but the out-of-class definition has to repeat all of them, in the same order, in both the declaration and the qualifier:

#include <iostream>
#include <string>

template <typename TFibre, typename TCount>
class Beam
{
private:
    TFibre m_fibre{};
    TCount m_count{};

public:
    Beam(const TFibre& fibre, const TCount& count)
        : m_fibre{ fibre }
        , m_count{ count }
    {
    }

    void report() const;
};

template <typename TFibre, typename TCount>
void Beam<TFibre, TCount>::report() const
{
    std::cout << m_fibre << " wound to " << m_count << " ends" << '\n';
}

int main()
{
    Beam<std::string, int> linenBeam{ "linen", 480 };
    linenBeam.report();

    Beam<std::string, double> woolBeam{ "wool", 62.5 };
    woolBeam.report();

    return 0;
}
linen wound to 480 ends
wool wound to 62.5 ends

Inside the class, the injected class name is still plain Beam, standing for Beam<TFibre, TCount>.

The Definition Has to Travel With the Class

A member function template is not compiled when you write it. It is compiled when someone instantiates it, and to do that the compiler must have both halves in front of it at that moment: the class definition, so it knows the function is a member, and the function's definition, so it knows what to generate.

That single requirement decides where the code goes. Put the class in a header, and every out-of-class member function definition must go in that same header, immediately below the class.

sett.h:

#pragma once

#include <iostream>

template <typename T>
class Sett
{
private:
    T m_ends{};
    T m_picks{};

public:
    Sett(const T& ends, const T& picks)
        : m_ends{ ends }
        , m_picks{ picks }
    {
    }

    void describe() const;
};

// defined immediately below the class, in the same header
template <typename T>
void Sett<T>::describe() const
{
    std::cout << m_ends << " ends / " << m_picks << " picks" << '\n';
}

warping.cpp:

#include "sett.h"

void dressLoom()
{
    Sett<int> tabby{ 24, 22 };
    tabby.describe();
}

main.cpp:

#include "sett.h"

void dressLoom();

int main()
{
    dressLoom();

    Sett<int> twill{ 30, 28 };
    twill.describe();

    return 0;
}

Compiling and linking both source files together:

24 ends / 22 picks
30 ends / 28 picks

Both translation units included the header, and both instantiated Sett<int>::describe(), which sounds like two definitions of the same function reaching the linker. It is not a problem, because functions instantiated from templates are implicitly inline. The linker sees the duplicates, recognises them as instantiations of the same template, and keeps one.

Key Concept
Implicit instantiation makes a function implicitly inline, for member function templates as much as for ordinary function templates. That is why putting member function template definitions in a header never breaks the one-definition rule, even when a dozen source files include it.

Move describe() into a sett.cpp instead and the arrangement collapses. main.cpp asks for Sett<int>::describe(), but the definition sits in a translation unit that never instantiates it for int, so nothing is generated anywhere. The linker output below is trimmed to the lines that matter, since it also names a temporary object file whose name changes on every build:

undefined reference to `Sett<int>::describe() const'
collect2: error: ld returned 1 exit status
Warning
A member function template defined in a source file compiles cleanly and then fails at link time. The error names a function you can plainly see you defined, which makes it a confusing one to debug. Keep the definitions in the header.
Best Practice
Define member function templates either inside the class or immediately below it in the same file. Anywhere the class definition is visible, the definitions will be too.

Defining member functions inside the class body avoids the question entirely, at the cost of a longer class definition. That is a readability trade-off rather than a correctness one, and either choice is fine.

Summary

The type parameter is in scope for the whole class: T can be the type of a data member, a member function parameter, a return type, or a local variable inside a member function.

Inside the class body, member functions need nothing extra: the template line written above the class already covers them.

Outside the class body, they need two things: a fresh template <typename T> declaration, and the fully templated name as the qualifier, so T Sett<T>::threadCount() const rather than T Sett::threadCount() const.

Skipping either one is a compile error: a missing declaration gives "T does not name a type"; a missing <T> reports that the class was "used without template arguments" and then that a non-member function cannot be const.

Injected class names: within a class template's own scope, the bare class name already carries its template arguments. That is why the constructor is named Sett, and why const Sett& works as a parameter type.

Class template argument deduction works without a deduction guide for a non-aggregate class, because a matching constructor already tells the compiler how to deduce the type arguments.

Multiple type parameters change nothing structurally: repeat all of them in the declaration and list all of them in the qualifier, as in Beam<TFibre, TCount>::report().

Definitions must be visible at the point of instantiation: put out-of-class member function definitions directly below the class in the same header. Putting them in a source file compiles but does not link.

Instantiated functions are implicitly inline: any number of translation units can include the header and instantiate the same specialization, and the linker will keep one copy.