Alternatives to templating the whole class

Viewed 76
struct S
{
    S(int);
    S(std::string);

    void foo(int);
    void foo(std::string)
};

So my problem is that foo() should be invokable only with the type the ctor was. Solutions I can think of and problems with them:

  • Template the whole class. However this brings all the implementation details to the header, polluting it with many library #includes, making the API hard to read, making error messages long.
  • bool is_int runtime check. The assert() could be forgotten when implementing this API. The constraint is not evident from the API.
  • Separate classes. Violates DRY or forces all implementation to be extracted in .cpp-local functions - which is not so readable nor intuitive.

What other solutions are there? Or which of mine is the least silly?

2 Answers

As I got you have already implemented S. You can use the pimpl or S as a base class. Rename S to SImpl and make another templated S.

simpl.h

struct SImpl
{
    SImpl(int);
    SImpl(std::string);

    void foo(int);
    void foo(std::string)
};

s.h

#include "simpl.h"

template <typename T>
struct S: protected SImpl
{
    S(T t) : SImpl(t) {}

    void foo(T t) { SImpl::foo(t); }
};

Morally, your class is a template, but will only be instantiated with a type from a finite, known set of types. Your question is how to avoid some disadvantages of templates you identified: implementation details leaked, larger translation units, unintelligible API, and worse error messages.

The most straightforward way to implement this is to define and instantiate the templates out-of-line:

// S.h
#pragma once
#include <string>
#include <type_traits>

template<typename T>
struct S
{
    static_assert(
        std::is_same_v<T, int> ||
        std::is_same_v<T, std::string>,
        "S<T> only supports T=int or T=std::string"
    );

    S(T);
    void foo(T);
};

extern template struct S<int>;
extern template struct S<std::string>

// S.cpp
#include "S.h"
#include <iostream>

template<typename T>
S<T>::S(T) { }

template<typename T>
void S<T>::foo(T t)
{
    std::cout << "foo(" << t << ")\n";
}

template struct S<int>;
template struct S<std::string>;

// main.cpp
#include "S.h"

int main()
{
    auto s = S{1};
    s.foo(2); // prints "foo(2)\n"
}

Any translation units using S do not instantiate, and cannot instantiate, specializations of S. They rely on the instantiations provided. Implementation details are not exposed. Translation units are only as large as necessary to define the interface, not to implement it. The static assertion isn't required, but facilitates an early and obvious error.

Related