9

I want to ensure, that a derived class implements a specific static method. I think doing so should be possible using static_assert, std::is_same, decltype, CRTP and maybe making use of SFINAE. However, similar code I found so far is quite complex and it seems I do not yet fully understand it making me unable to adopt it to my needs.

What I tried so far is this

template <class T>
class Base 
{
    static_assert(std::is_same<decltype(T::foo(1)), int>::value, "ERROR STRING");
};

class Derived : public Base <Derived>
{
public:
    static int foo(int i) { return 42; };
};

However, it does not compile telling me, that Derived does no have an element named foo even if the method is correctly implemented. Furthermore providing actual parameters for foo in the expression inside static_assert feels wrong.

Searching SO revealed a similar question which finally lead me to this piece of code where it is checked that a type has methods begin() and end() returning iterators. So I tried to adopt this code to my needs.

template <class T>
class Base 
{
    template<typename C>
    static char(&g(typename std::enable_if<std::is_same<decltype(static_cast<int(C::*)(int)>(&C::foo)), int(C::*)(int)>::value, void>::type*))[1];

    template<typename C>
    static char(&g(...))[2];

    static_assert(sizeof(g<T>(0)) == 1, "ERROR STRING");
};

But this code does not compile because the assertion fires.

So my questions are

  1. Why does the compiler cannot find Derived::foo in my first example?
  2. What exactly does typename C::const_iterator(C::*)() const in the example code mean? Isn't it a const function returning C::const_iterator and taking no arguments? What exactly does C::* mean? So why is int(C::*)(int) then wrong in my case?
  3. How to correctly solve my problem?

I'am using MSVC 12 but if possible the code should be portable.

sigy
  • 2,408
  • 1
  • 24
  • 55
  • 1
    You're not a parser, don't tokenize your code! :) Templates are quite tricky to read already; splitting a line into *too* many lines just makes it worse. – Rubens Apr 29 '14 at 14:19
  • 2
    `"ERROR STRING"` is a poor choice of message for assertion failure. The correct choice is ["Jabberwocky is killing user."](http://stackoverflow.com/questions/21553327/why-is-except-pass-a-bad-programming-practice/21553515#21553515) – Casey Apr 29 '14 at 16:20
  • As Base and Derived are bad class names and foo is a bad method name. ;) But this was meant as SSCCE and I considered the string as not important. However, "ERROR STRING" was intended to tell that there is some real error string. I chose something like "T needs to implement a method with signature ...". But thanks for the comment anyway. :) – sigy Apr 29 '14 at 16:48

1 Answers1

13

This is a common problem when using CRTP: Base<Derived> is instantiated at the point where it is encountered in Derived's list of bases, at which time Derived is not yet a complete type since the rest of its declaration hasn't been parsed yet. There are various workarounds. For static_assert, you need to delay instantiation of the assertion until Derived is complete. One way to do so is to put the assertion in a member function of Base that you know must be instantiated - the destructor is always a good choice (Live at Coliru):

template <class T>
class Base 
{
public:
    ~Base() {
        static_assert(std::is_same<decltype(T::foo(1)), int>::value, "ERROR STRING");
    }
};

class Derived : public Base<Derived>
{
public:
    static int foo(int) { return 42; };
};

Addressing question #2: C::* is the syntax for "pointer to member of class C." So int(*)(int) is "pointer to function taking a single int parameter and returning int", and int(C::*)(int) is analogously "pointer to member function of C taking a single int parameter and returning int." The monstrosity

typename C::const_iterator(C::*)() const

would translate to "pointer to constant member function of C taking no parameters and returning C::const_iterator" where of course the typename is necessary to indicate that the dependent name C::const_iterator is a type.

Casey
  • 41,449
  • 7
  • 95
  • 125
  • Of course! Why do I run into the same mistake over and over again? Thank you for pointing this out to me (again) ;) However, it still feels odd to provide actual parameters for foo() :/ – sigy Apr 29 '14 at 14:30
  • 1
    @sigy If it really bothers you, you could use `std::declval` to make a "generic" integer rvalue expression: `static_assert(std::is_same())), int>::value, "ERROR STRING");`. You might also consider `is_convertible` instead of `is_same`. – Casey Apr 29 '14 at 14:33
  • Thanks again. As long as there is no answer tackling all questions, I will accept your answer. It solves my problem. – sigy Apr 29 '14 at 14:54
  • @sigy There's an explanation of the syntax in question 2, didn't realize I'd forgotten to address it. – Casey Apr 29 '14 at 15:18
  • Thanks, but why is `int(C::*)(int)` wrong then? And what if my function needs an lvalue as parameter? I am unable to use declval then. – sigy Apr 29 '14 at 15:38
  • 3
    `int(C::*)(int)` is wrong because the type of `&Derived::foo` is `int(*)(int)` because it is a `static` member function. If you need an lvalue you can do `std::declval()`. – Oktalist Apr 29 '14 at 16:17
  • 3
    There is a slight issue with this answer: writing out a destructor has the consequence of disabling the generation of a move constructor and move assignment operator. It also prevents the object from being [trivially_destructible](http://en.cppreference.com/w/cpp/types/is_destructible). So, while it works, there are downsides to the approach (and such downsides apply to other special members). – Matthieu M. Apr 29 '14 at 16:32
  • @Oktalist: Ok, the type of &Derived::foo makes total sense. But std::declval() doesn't work for me. To be specific I need an lvalue of an object type and it look like `add_rvalue_reference` causes some problems here. @MatthieuM. Any suggestions? In my case the class is even abstract. Just creating some private dummy function? Should solve the problem but feels like bad practice. – sigy Apr 29 '14 at 16:53
  • 1
    @sigy I have no problem with `std::declval()` where `Foo` is a class type. I don't know why `add_rvalue_reference` would cause any problem. Maybe you should create a new question for that. – Oktalist Apr 29 '14 at 18:43