AStupidNoob
AStupidNoob

Reputation: 2040

C++ templates: conditionally enabled member function

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)?

Upvotes: 10

Views: 11465

Answers (3)

YSC
YSC

Reputation: 40060

Simple answer

This answer bares parallels to Yakk's answer but is not quite as useful (Yakk's one supports arbitrary if-expressions). It is, however, quite a bit simpler and easier to understand.

template <typename ThisClass, typename ElementType>
class WorkingSimpleVector_Base {

};

template <typename ThisClass>
class WorkingSimpleVector_Base<ThisClass, char> {
private:
    ThisClass * me() { return static_cast<ThisClass*>(this); };
    const ThisClass * me() const { return static_cast<const ThisClass*>(this); };
public:
    bool operator==(const std::string & other) const {
        if (me()->count_ == other.length())
        {
            return memcmp(me()->elements_, other.c_str(), other.length()) == 0;
        }
        return false;
    }
};

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

This works by utilising template specialization for the 'if statements' we want. We base the class off of WorkingSimpleVector_Base, which then only contains functions if the ElementType value is char (second definition of WorkingSimpleVector_Base). Otherwise, it has no functions at all (first definition of WorkingSimpleVector_Base). The ThisClass parameter is what makes this a "CRTP" (Curiously Recurring Template Pattern). It allows the template to access the child class's fields through the use of the me() function. Remember that the template is no different from any other class so it won't have access to private members (unless the child class declares it as a friend).


Modified version of Yakk's answer explained

This first thing he/she declares is a helper template that does this whole conditional declaration for us:

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;

Variadic templates are scary, and I think they can be removed in this case. Let's simplify this to:

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

template<class X>
struct conditional_apply_t<true, X> {
  using type = X;
};

template<bool test, class X>
using conditional_apply=typename conditional_apply_t<test, X>::type;

conditional_apply_t's type type is an empty struct if the condition test is not true (see first definition of conditional_apply_t). If it is true, then the type type is the value of X. The definition of conditional_apply just removes the need for us to write ::type at the end of conditional_apply_t<...> every time we use this construct.

Next, we define a template that implements the behaviour we want

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;
  }
};

In this case, the D parameter is what gives us the "CRTP" (Curiously Recurring Template Pattern). See the above "Simple Answer" for more details on why this is important.

Next, we declare a type that only has this operator== function if a condition is satisfied:

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

So, the equal_string_helper<D,ET> type is:

  • An empty struct when ET != char
  • equal_string_helper_t<D> when ET == char

Finally, after all this, we can create the class we wanted with the following:

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

Which works as required.

Upvotes: 0

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275220

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?

Upvotes: 5

Barry
Barry

Reputation: 302663

Templating operator== basically makes it uncallable. You'd have to explicitly do:

myvec.operator==<char>(str);

The simplest solution might just to add a non-member function:

bool operator==(const WorkingVector<char>& vec, const std::string& s);
bool operator==(const std::string& s, const WorkingVector<char>& vec);

To enable SFINAE and to keep it as a member function, you'd have to forward your type to something else:

bool operator==(const std::string& s) const {
    return is_equal<ElementType>(s);
}

template <typename T> // sure, T == ET, but you need it in this context
                      // in order for SFINAE to apply
typename std::enable_if<std::is_same<T, char>::value, bool>::type
is_equal(const std::string& s) {
    // stuff
}

Upvotes: 3

Related Questions