5

Q: Are there are any compile time mechanisms in C++ I can use to automatically verify that the set of template class methods matches from class specialization to specialization?

Example: Let us suppose I want a class interface that has very different behaviors depending on a template value specialization:

// forward declaration of template class name
template <typename T, bool isZero> class MyClass;

// partial class specialization for isZero = false case
template <typename T> class MyClass <T, false>
{
   T _val;

public:
   MyClass(T value) { _val = value; }
   T GetValue(void) const { return _val; }
};

// partial class specialization for isZero = true case
template <typename T> class MyClass <T, true>
{
public:
   MyClass(T value) {}
   T GetValue(void) const {return T(0);}
};

The idea here is that when compiling on Tuesdays, MyClass.GetValue() returns 0, whereas any other day of the week, we get the expected value ...or some such thing. The details and motivations are unimportant. ;-)

Now, while this seems like one way to achieve partial function specialization based on partial class specialization, it also seems like it can also be utter chaos because the two class specializations can (AFAIK) have completely inconsistent interfaces. If I am actually going to use this mechanism in a production setting, I would like some ideally compile time notification if I mistakenly add some methods to some specializations and not others or forget a const somewhere, etc. How do I get that?

For bonus points, let us suppose I don't want to accidentally allow other values of isZero beyond (true/false) to compile as might occur if I provided a generic implementation of MyClass and let us also suppose I am suspicious of added runtime cost from adding virtual here for pure virtual base class methods.

This seems like such an obvious language feature that I'm likely missing the forest for the trees here and maybe I'm already getting this behavior through some mechanism and haven't realized it yet.

> cat test.cpp                   
#include <stdio.h>

// forward declaration of template class name
template <typename T, bool isZero> class MyClass;

// partial specialization for isZero = false case
template <typename T> class MyClass <T, false>
{
   T _val;

public:
   MyClass(T value) { _val = value; }
   T GetValue(void) const { return _val; }
};

// partial specialization for isZero = true case
template <typename T> class MyClass <T, true>
{
public:
   MyClass(T value) {}
   T GetValue(void) const {return T(0);}
};

int main( void )
{
   MyClass<float, false>  one(1);
   MyClass<float, true> notOne(1);

   printf( "%f, %f\n", one.GetValue(), notOne.GetValue());
   return 0;
}
> clang -Wall -pedantic test.cpp 
> ./a.out                        
1.000000, 0.000000
Ian Ollmann
  • 1,592
  • 9
  • 16
  • Other values for `isZero` are impossible in this case, because the type is `bool`. This would make more sense with e.g. `int`. However, I think that this is a different question, that should be asked separately (if there isn't already a duplicate). – walnut Sep 20 '19 at 01:36
  • If you add a method to one specialization, but not the other, there will be an error if you try to use that method with the other class. If you don't get the error you're not using the new method so it doesn't matter if you don't call it. – 1201ProgramAlarm Sep 20 '19 at 01:50
  • Related to [does-static-polymorphism-make-sense-for-implementing-an-interface](https://stackoverflow.com/questions/20771210/does-static-polymorphism-make-sense-for-implementing-an-interface) – Jarod42 Sep 20 '19 at 08:07

2 Answers2

3

You could make a static assertion at the point of use:

template <typename T>
class MyOtherClass
{
    static_assert(std::is_same_v<decltype(MyClass<T, true >{T{}}.GetValue()), T>);
    static_assert(std::is_same_v<decltype(MyClass<T, false>{T{}}.GetValue()), T>);

    ... 

};

You could also attempt to define a traits class that defines/identifies the interface, if you find yourself making this assertion in several places.

Because you're leaving a template parameter unspecialized (namely T), the traits are a little awkward, but these might work:

// This traits class will inherit from either 
// std::true_type or std::false_type.

template <template <typename, bool> S, typename T>
struct is_value_provider : std::intergral_constant<bool, 
    std::is_same_v<decltype(S<T, true >{T{}}.getValue()), T> &&
    std::is_same_v<decltype(S<T, false>{T{}}.getValue()), T>>
{}

template <template <typename, bool> S, typename T>
using is_value_provider_v = is_value_provider::value;


// Usage examples:
static_assert(is_value_provider_v<MyClass, int>);
static_assert(is_value_provider_v<MyClass, float>);
static_assert(is_value_provider_v<MyClass, double>);
NicholasM
  • 4,557
  • 1
  • 20
  • 47
2

Unit tests perhaps?

With Catch2, you could use TEMPLATE_TEST_CASE_SIG to compile the same test case for different specializations. You could express the similarities between specializations using "actual code" instead of traits.

It might look something like this:

TEMPLATE_TEST_CASE_SIG(
  "MyClass specialization compat", 
  "[myclass]", 
  ((typename T, bool isZero), T, isZero),
  (int, false), (int, true)
) {
  const MyClass<T, isZero> mc{T{}};
  T val{mc.GetValue()};
}

This will ensure that GetValue exists, is marked const and returns something that can be narrowed to T. You don't have to use a unit testing framework. You can just use a function template. You don't need to actually run the tests. You just need to compile them.

template <typename T, bool isZero>
void test() {
  const MyClass<T, isZero> mc{T{}};
  T val = mc.GetValue();
}

inline void runTests() {
  test<int, false>();
  test<int, true>();
}

If you want to be more thorough, you end up with the same tedium as in Nicholas's answer.

Indiana Kernick
  • 5,041
  • 2
  • 20
  • 50