1

I want to write a function that accepts a collection of type T, say std::vector<T>, but that does two different things depending on T. For example, if T is == comparable, then use a == b, else if T has a .value element, use that (a.value == b.value).

My first attempt was to use an overloaded function, but that fails if I pass in a derived class (subclass) of T.

Suppose, for example, I want to create an Exists method. (I know this can be implemented using std::find_if; it is an example only.) The following fails to compile:

using namespace std;

struct Base {
    Base(string s) : value(std::move(s)) {}
    string value;
};

struct Derived : public Base {
    Derived(string s) : Base(std::move(s)) {}
};

bool Exists(const vector<string>& collection, const string& item) {
    for (const auto& x : collection)
        if (x == item)
            return true;
    return false;
}

bool Exists(const vector<Base>& collection, const Base& item) {
    for (const auto& x : collection)
        if (x.value == item.value)
            return true;
    return false;
}

This works fine for exact matches, such as:

Exists(vector<string>{"a", "b", "c"}, "b");
Exists(vector<Base>{{"a"}, {"b"}, {"c"}}, Base{"b"});

But it fails for derived classes:

Exists(vector<Derived>{{"a"}, {"b"}, {"c"}}, Derived{"b"})

The error is:

foo.cc:35:13: error: no matching function for call to 'Exists'
foo.cc:23:6: note: candidate function not viable: no known conversion from 'vector<Derived>' to 'const vector<Base>' for
    1st argument

How can I solve this? I am interested in multiple answers, since each solution probably has pros and cons.

Mark Lodato
  • 50,015
  • 5
  • 41
  • 32

3 Answers3

2

This is probably not a duplicate per se, but very close to this: Is it possible to write a template to check for a function's existence?

My recommended approach is the more general solution implemented in that answer: use SFINAE.

The snippet of how to test for a member function is below (adapted from here):

template <class T>
class has_value {
  template <class M>
  static inline bool try_match(decltype(&M::value)) { }
  template <class M>
  static inline int  try_match(...) { }
public:
  static constexpr bool value =
    sizeof(try_match<T>(nullptr)) == sizeof(bool);
};

this can then be combined with std::enable_if to solve your problem. I have posted a full solution as a GitHub gist.

In my opinion, this is superior to using base and inheritance checks as it works by simply checking (at compile-time) whether a given type has a given member. Additionally, it works for anything that has a type, meaning members, functions, static members / functions, types, etc.

Community
  • 1
  • 1
Myles Hathcock
  • 443
  • 2
  • 11
  • Ah, interesting. This is basically an expansion of my `std::enable_if` answer, which uses `std::is_base_of` rather than your `has_value`. I think this is extremely hard to read, but it is definitely good to know in case no other options are suitable. – Mark Lodato May 25 '15 at 03:04
  • Agreed, its definitely not straightforward. But as you said, its value is that it is the most general, and can be resorted to when no other options are available (or you do not wish to pay the runtime overhead of virtual calling). – Myles Hathcock May 25 '15 at 03:15
  • Oupsie ! The Gist has disappeared... :'( – Chucky Nov 07 '22 at 08:36
1

One solution is to template the Exists() method and then have an overloaded comparison function. This only works if the type-specific code can be isolated. For example:

bool Equals(const string& a, const string& b) { return a == b; }
bool Equals(const Base& a, const Base& b) { return a.value == b.value; }

template <typename T>
bool Exists(const vector<T>& collection,
            const typename vector<T>::value_type& item) {
    for (const auto& x : collection)
        if (Equals(x, item))
            return true;
    return false;
}

Pro: Probably the simplest solution.

Con: Does not work if you need to do some sort of expensive work up front. For example, if you need to call x.SomeExpensiveMethod() and you want to cache it for the item argument, this will not work.

Note that you need to use vector<t>::value_type and not just T in the argument or else you may get an error such as:

 foo3.cc:30:13: error: no matching function for call to 'Exists'
     cout << Exists(vector<string>{"a", "b", "c"}, "b") << endl;
             ^~~~~~
 foo3.cc:21:6: note: candidate template ignored: deduced conflicting types for parameter 'T' ('std::basic_string<char>' vs.
      'char [2]')
Mark Lodato
  • 50,015
  • 5
  • 41
  • 32
0

One solution is to use std::enable_if and std::is_base_of. For example:

template <typename T>
typename std::enable_if<std::is_base_of<Base, T>::value, bool>::type
Exists(const vector<T>& collection,
    const typename vector<T>::value_type& item) {
    const auto& item_cached = item.SomeExpensiveFunction();
    for (const auto& x : collection)
        if (x.SomeExpensiveFunction() == item_cached)
            return true;
    return false;
}

template <typename T>
typename std::enable_if<!std::is_base_of<Base, T>::value, bool>::type
Exists(const vector<T>& collection,
    const typename vector<T>::value_type& item) {
    for (const auto& x : collection)
        if (x == item)
            return true;
    return false;
}

Pro: Much more general than overloading the Equals() function as described in another answer. In particular, the entire Exists() method can be customized per type.

Con: Much uglier, more complicated code.

Mark Lodato
  • 50,015
  • 5
  • 41
  • 32