C++ templates: conditionally enabled member function

AStupidNoob picture AStupidNoob · Oct 29, 2014 · Viewed 7.2k times · Source

I'm creating a very small C++ project, and I'd like to create a simple vector class for my own needs. The std::vector template class will not do. When the vector class is comprised of chars (i.e. vector<char>), I'd like it to be able to be compared to a std::string. After a bit of messing around, I wrote code that both compiles and does what I want. See below:

#include <string>
#include <stdlib.h>
#include <string.h>

template <typename ElementType>
class WorkingSimpleVector {
 public:
    const ElementType * elements_;
    size_t count_;

    // ...

    template <typename ET = ElementType>
    inline typename std::enable_if<std::is_same<ET, char>::value && std::is_same<ElementType, char>::value, bool>::type
    operator==(const std::string & other) const {
        if (count_ == other.length())
        {
            return memcmp(elements_, other.c_str(), other.length()) == 0;
        }
        return false;
    }
};

template <typename ElementType>
class NotWorkingSimpleVector {
 public:
    const ElementType * elements_;
    size_t count_;

    // ...

    inline typename std::enable_if<std::is_same<ElementType, char>::value, bool>::type
    operator==(const std::string & other) const {
        if (count_ == other.length())
        {
            return memcmp(elements_, other.c_str(), other.length()) == 0;
        }
        return false;
    }
};

int main(int argc, char ** argv) {
    // All of the following declarations are legal.
    WorkingSimpleVector<char> wsv;
    NotWorkingSimpleVector<char> nwsv;
    WorkingSimpleVector<int> wsv2;
    std::string s("abc");

    // But this one fails: error: no type named ‘type’ in ‘struct std::enable_if<false, bool>’
    NotWorkingSimpleVector<int> nwsv2;

    (wsv == s);  // LEGAL (wanted behaviour)
    (nwsv == s);  // LEGAL (wanted behaviour)
    // (wsv2 == s);  // ILLEGAL (wanted behaviour)
    // (nwsv2 == s);  // ??? (unwanted behaviour)
}

I believe I understand why the error is occurring: The compiler creates the class definition for NotWorkingSimpleVector<int>, and then the return type of my operator== function becomes:

std::enable_if<std::is_same<int, char>::value, bool>::type

which then becomes:

std::enable_if<false, bool>::type

which then yields an error: there is no type member of std::enable_if<false, bool>, which is indeed the entire point of the enable_if template.

I have two questions.

  1. Why won't SFINAE simply disable the definition of operator== for NotWorkingSimpleVector<int>, like I want it to? Is there some compatibility reason for this? Are there other use-cases I'm missing; does a reasonable counter-argument for this behaviour exist?
  2. Why does the first class (WorkingSimpleVector) work? It seems to me that the compiler 'reserves judgement': Since the 'ET' parameter is not yet defined, it gives up trying to tell if operator== can exist. Are we relying on the compilers 'lack of insight' to allow this kind of conditionally enabled function (even if this 'lack of insight' is acceptable by the C++ specification)?

Answer

Yakk - Adam Nevraumont picture Yakk - Adam Nevraumont · Oct 29, 2014

SFINAE works in a template function. In the context of template type substitution, substitution failure in the immediate context of the substitution is not an error, and instead counts as substitution failure.

Note, however, that there must be a valid substitution or your program is ill formed, no diagnostic required. I presume this condition exists in order that further "more intrusive" or complete checks can be added to the language in the future that check the validity of a template function. So long as said checks are actually checking that the template could be instantiated with some type, it becomes a valid check, but it could break code that expects that a template with no valid substitutions is valid, if that makes sense. This could make your original solution an ill formed program if there is no template type you could pass to the operator== function that would let the program compile.

In the second case, there is no substitution context, so SFINAE does not apply. There is no substitution to fail.

Last I looked at the incoming Concepts proposal, you could add requires clauses to methods in a template object that depend on the template parameters of the object, and on failure the method would not be considered for overload resolution. This is in effect what you want.

There is no standards-compliant way to do this under the current standard. The first attempt is one that may people commonly do, and it does compile, but it is technically in violation of the standard (but no diagnostic of the failure is required).

The standards compliant ways I have figured out to do what you want:

Changing one of the parameters of the method to be a reference to a never-completed type if your condition fails. The body of the method is never instantiate if not called, and this technique prevents it from being called.

Using a CRTP base class helper that uses SFINAE to include/exclude the method depending on an arbitrary condition.

template <class D, class ET, class=void>
struct equal_string_helper {};

template <class D, class ET>
struct equal_string_helper<D,ET,typename std::enable_if<std::is_same<ET, char>::value>::type> {
  D const* self() const { return static_cast<D const*>(this); }
  bool operator==(const std::string & other) const {
    if (self()->count_ == other.length())
    {
        return memcmp(self()->elements_, other.c_str(), other.length()) == 0;
    }
    return false;
  }
};

where we do this:

template <typename ElementType>
class WorkingSimpleVector:equal_string_helper<WorkingSimpleVector,ElementType>

We can refactor the conditional machinery out of the CRTP implementation if we choose:

template<bool, template<class...>class X, class...>
struct conditional_apply_t {
  struct type {};
};

template<template<class...>class X, class...Ts>
struct conditional_apply_t<true, X, Ts...> {
  using type = X<Ts...>;
};
template<bool test, template<class...>class X, class...Ts>
using conditional_apply=typename conditional_apply_t<test, X, Ts...>::type;

Then we split out the CRTP implementation without the conditional code:

template <class D>
struct equal_string_helper_t {
  D const* self() const { return static_cast<D const*>(this); }
  bool operator==(const std::string & other) const {
    if (self()->count_ == other.length())
    {
        return memcmp(self()->elements_, other.c_str(), other.length()) == 0;
    }
    return false;
  }
};

then hook them up:

template<class D, class ET>
using equal_string_helper=conditional_apply<std::is_same<ET,char>::value, equal_string_helper_t, D>;

and we use it:

template <typename ElementType>
class WorkingSimpleVector: equal_string_helper<WorkingSimpleVector<ElementType>,ElementType>

which looks identical at point of use. But the machinery behind was refactored, so, bonus?