7

I have a template class/struct that looks like this:

template <typename T, typename U>
struct S
{
    unsigned int operator()(T t, U u) const;
};

And I would like to make sure that specializations respect this interface.

Unfortunately, I can specialize this struct with a different return type. For instance if I partial-specialize to return bool instead of unsigned int, I would expect to get a compiler error, but the compiler does not seem to care:

template <typename T>
struct S<T,nullptr_t>
{
    bool operator()(T t, nullptr_t u) const { return 2; }
};

Example @ Ideone.com

In the above example, the specialized version is supposed to return 2 but since the return type is bool, the return value is converted to true which is then displayed as 1.

Why does the compiler accept this?

How can I prevent programmers from specializing the template with a wrong return type (or even with wrong arguments)?

I know I can achieve what I want with a virtual method in a base template class/struct and use the override keyword in children:

Solution with override (does not compile, which is good)

But having a virtual method will certainly create a virtual table and I would like to avoid that as much as possible, especially since I do not need the virtual table at run-time. Unless there is a trick to do the same without building a virtual table?

Also, I know the problem would be simpler if I could partial-specialize methods or if I could rely on non-partial-specialization, but the former is not possible in C++ AFAIK and the latter does not cover the case I need in my program.

vdavid
  • 2,434
  • 1
  • 14
  • 15
  • 1
    "How can I prevent programmers from specializing the template with a wrong return type" you cannot. Programmers can write specialisations that don't resemble your original template at all, and use them however they want. The only thing you can do is make sure functions *you* write only accept "good" specialisations. – n. m. could be an AI Aug 09 '18 at 09:25
  • I think you can take a look at https://en.cppreference.com/w/cpp/types/result_of and do some static assert if the deduced type is what you want. If it is wrong the static assert will stop compilation with error. – wdudzik Aug 09 '18 at 09:29
  • @wdudzik I thought of checking the result type (with `decltype` or `std::result_of`) but then it means either every caller would need to check it or that every specialization would need to check it, which is a bit heavy. Maybe I could make that check automatic with C++20's concepts but my compiler is not compatible with C++20. – vdavid Aug 09 '18 at 09:45
  • Why do you care? Sure, people can write crazy stuff, but they could've even without specializing something insensible. – Passer By Aug 09 '18 at 10:50
  • @PasserBy I do care for the same reason that the `override` keyword was introduced: making sure an interface is respected when the code is written, and that the interface is also respected when it is updated later on. – vdavid Aug 09 '18 at 14:57
  • There is a major difference here, a virtual function with incorrect signature will fail silently, and its condition for failure may depend on code arbitrarily distant. This, on the other hand, will typically be a compile error and its condition for failure is local. If anything, the real problem here is implicit conversions. – Passer By Aug 10 '18 at 07:29

1 Answers1

2

A good way to create static interface is usage of curiously recurring template pattern. In Your situation it would look like this:

template<class Derived, class T, class U>
constexpr bool MyTrait = std::is_same<unsigned int, decltype(std::declval<Derived>()(std::declval<T>(), std::declval<U>()))>::value;

template <typename Derived, class T, class U>
struct StaticInterface
{

    unsigned int operator()(T t, U u) const{
        static_assert( MyTrait<Derived, T, U>, "errr" );

        return (*static_cast<const Derived *>(this))(std::forward<T>(t), std::forward<U>(u));
    }
};

template <typename T, typename U>
struct S : StaticInterface<S<T, U>, T, U>
{
    unsigned int operator()(T t, U u) const{ /* some implementation */}
};

template <typename T>
struct S<T, std::nullptr_t> : StaticInterface<S<T, std::nullptr_t>, T, std::nullptr_t>
{
    bool operator()(T t, std::nullptr_t u) const { return 2; }
};

To get it working the function call must be done via interface like this:

template<class Derived, class T, class U>
void test(const StaticInterface<Derived, T, U> &inter){
    inter(T(), U());
}

Otherwise the operator from derived will be picked as the preferable one.

bartop
  • 9,971
  • 1
  • 23
  • 54
  • The OP explicitely asked for the compiler to reject specialization that does not have the correct return type, which is not the case in your answer – cmourglia Aug 09 '18 at 12:13
  • @Zouch fixed as You requested – bartop Aug 09 '18 at 12:29
  • That's an interesting approach but it still allows the return type to be changed without compiler errors. https://ideone.com/H27dRk I think this is because the `operator()` of the base class is not involved in the template resolution, only the child `operator()` is. – vdavid Aug 09 '18 at 13:57
  • @vdavid Naturally it should be used via interface to validate properly – bartop Aug 09 '18 at 14:02
  • @bartop ok so with an intermediate function it could go that way https://ideone.com/cJpgan but then I could as well do the same with an intermediate function in the non-CRTP implementation, e.g. https://ideone.com/kiJZHd – vdavid Aug 09 '18 at 14:36
  • @vdavid This is an example. What I mean is making the functions use the interface, not the concrete type. This will ensure proper operator signature – bartop Aug 09 '18 at 14:53