revisiting enable_if

It was roughly 2008, when I wanted to make a template function for serialization, only available to container types. Template stuff can become complicated at times, and from reading the documentation boost::enable_if seemed to be just what I needed. I didn’t get it to work, and I blamed Microsoft Visual Studio 2005 for not being standards compatible enough. And somehow I remembered enable_if as being difficult and hard to get to work, despite highly desirable if it would work. I ended up providing explicit template overloads for all the supported container types.

Fast forward to five years later, enable_if made it into the C++11 standard, and I didn’t even notice until reading “The C++ programming language” by Barne Strousup. In the book the facility is presented as a concise template that is easy to use and even to implement. To understand it’s value, let’s start with an example. Suppose, I want to implement a template function to stream the contents of containers to stdout.

#include <iostream>
#include <vector>
#include <list>

template<class ContainerT, class StreamT>
StreamT& operator<<(StreamT& strm, const ContainerT& cont)
{
	strm << '{';
	for(const auto& element : cont)
		strm << element << " ";
	strm << "} ";
        return strm;
}

int main()
{
	std::vector<int> ints{8, 45, 87, 90, 99999};
	std::list<float> floats{3.14159, 2.71828, 0.57721, 1.618033};
	std::cout << ints << floats;

	return 0;
}

So far so good, this does the trick. And the output is just what we expected: {8 45 87 90 99999 } {3.14159 2.71828 0.57721 1.61803 } But now we also write an output stream operator for some user defined interface type.

#include <iostream>
#include <sstream>
#include <vector>
#include <list>

template<class ContainerT, class StreamT>
StreamT& operator<<(StreamT& strm, const ContainerT& cont)
{
	strm << '{';
	for(const auto& element : cont)
		strm << element << " ";
	strm << "} ";
        return strm;
}

struct Base
{
    virtual std::string Describe() const = 0;
};

std::ostream& operator<<(std::ostream& strm, const Base& base)
{
    strm << base.Describe() << " ";
}

struct SomeType : public Base
{
    SomeType(int p1, int p2) : one_(p1), another_(p2) { }

    virtual std::string Describe() const
    {
        std::stringstream sstr;
        sstr << "{" << one_ << " " << another_ << "}";
        return sstr.str();
    }

	int one_;
	int another_;
};

int main()
{
	std::vector<int> ints{8, 45, 87, 90, 99999};
	std::list<float> floats{3.14159, 2.71828, 0.57721, 1.618033};
	std::cout << ints << floats;

	SomeType some{456, 789};
	std::cout << some;

	return 0;
}

Looks inconspicuous, and should work. Right? What do you think happens?

/home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp: In Instanziierung von »StreamT& operator<<(StreamT&, const ContainerT&) [mit ContainerT = int; StreamT = std::basic_stringstream<char>]«:
/home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp:34:24:   von hier erfordert
/home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp:12:2: Fehler: keine passende Funktion für Aufruf von »begin(const int&)«
/home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp:12:2: Anmerkung: Kandidaten sind:
In file included from /usr/include/c++/4.7/string:53:0,
                 from /usr/include/c++/4.7/bits/locale_classes.h:42,
                 from /usr/include/c++/4.7/bits/ios_base.h:43,
                 from /usr/include/c++/4.7/ios:43,
                 from /usr/include/c++/4.7/ostream:40,
                 from /usr/include/c++/4.7/iterator:64,
                 from /usr/include/boost/iterator.hpp:17,
                 from /usr/include/boost/concept_check.hpp:22,
                 from /usr/include/boost/range/concepts.hpp:19,
                 from /home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp:1:
/usr/include/c++/4.7/bits/range_access.h:87:5: Anmerkung: template<class _Tp, long unsigned int _Nm> _Tp* std::begin(_Tp (&)[_Nm])
/usr/include/c++/4.7/bits/range_access.h:87:5: Anmerkung:   Herleitung/Ersetzung von Templateargument gescheitert:
/home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp:12:2: Anmerkung:   unpassende Typen »_Tp [_Nm]« und »const int«
In file included from /usr/include/c++/4.7/string:53:0,
                 from /usr/include/c++/4.7/bits/locale_classes.h:42,
                 from /usr/include/c++/4.7/bits/ios_base.h:43,
                 from /usr/include/c++/4.7/ios:43,
                 from /usr/include/c++/4.7/ostream:40,
                 from /usr/include/c++/4.7/iterator:64,
                 from /usr/include/boost/iterator.hpp:17,
                 from /usr/include/boost/concept_check.hpp:22,
                 from /usr/include/boost/range/concepts.hpp:19,
                 from /home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp:1:
/usr/include/c++/4.7/bits/range_access.h:58:5: Anmerkung: template<class _Container> decltype (__cont.begin()) std::begin(const _Container&)
/usr/include/c++/4.7/bits/range_access.h:58:5: Anmerkung:   Herleitung/Ersetzung von Templateargument gescheitert:
/usr/include/c++/4.7/bits/range_access.h: In Ersetzung von »template<class _Container> decltype (__cont.begin()) std::begin(const _Container&) [mit _Container = int]«:
/home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp:12:2:   erfordert durch »StreamT& operator<<(StreamT&, const ContainerT&) [mit ContainerT = int; StreamT = std::basic_stringstream<char>]«
/home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp:34:24:   von hier erfordert
/usr/include/c++/4.7/bits/range_access.h:58:5: Fehler: Abfrage des Elementes »begin« in »__cont«, das vom Nicht-Klassentyp »const int« ist
/home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp: In Instanziierung von »StreamT& operator<<(StreamT&, const ContainerT&) [mit ContainerT = int; StreamT = std::basic_stringstream<char>]«:
/home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp:34:24:   von hier erfordert
/usr/include/c++/4.7/bits/range_access.h:48:5: Anmerkung: template<class _Container> decltype (__cont.begin()) std::begin(_Container&)
/usr/include/c++/4.7/bits/range_access.h:48:5: Anmerkung:   Herleitung/Ersetzung von Templateargument gescheitert:
/usr/include/c++/4.7/bits/range_access.h: In Ersetzung von »template<class _Container> decltype (__cont.begin()) std::begin(_Container&) [mit _Container = const int]«:
/home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp:12:2:   erfordert durch »StreamT& operator<<(StreamT&, const ContainerT&) [mit ContainerT = int; StreamT = std::basic_stringstream<char>]«
/home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp:34:24:   von hier erfordert
/usr/include/c++/4.7/bits/range_access.h:48:5: Fehler: Abfrage des Elementes »begin« in »__cont«, das vom Nicht-Klassentyp »const int« ist
In file included from /usr/include/c++/4.7/utility:76:0,
                 from /usr/include/boost/config/no_tr1/utility.hpp:21,
                 from /usr/include/boost/config/select_stdlib_config.hpp:37,
                 from /usr/include/boost/config.hpp:40,
                 from /usr/include/boost/concept/assert.hpp:7,
                 from /usr/include/boost/concept_check.hpp:20,
                 from /usr/include/boost/range/concepts.hpp:19,
                 from /home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp:1:
/home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp: In Instanziierung von »StreamT& operator<<(StreamT&, const ContainerT&) [mit ContainerT = int; StreamT = std::basic_stringstream<char>]«:
/home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp:34:24:   von hier erfordert
/usr/include/c++/4.7/initializer_list:89:5: Anmerkung: template<class _Tp> constexpr const _Tp* std::begin(std::initializer_list<_Tp>)
/usr/include/c++/4.7/initializer_list:89:5: Anmerkung:   Herleitung/Ersetzung von Templateargument gescheitert:
/home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp:12:2: Anmerkung:   unpassende Typen »std::initializer_list<_Tp>« und »int«
/home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp:12:2: Fehler: keine passende Funktion für Aufruf von »end(const int&)«
/home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp:12:2: Anmerkung: Kandidaten sind:
In file included from /usr/include/c++/4.7/string:53:0,
                 from /usr/include/c++/4.7/bits/locale_classes.h:42,
                 from /usr/include/c++/4.7/bits/ios_base.h:43,
                 from /usr/include/c++/4.7/ios:43,
                 from /usr/include/c++/4.7/ostream:40,
                 from /usr/include/c++/4.7/iterator:64,
                 from /usr/include/boost/iterator.hpp:17,
                 from /usr/include/boost/concept_check.hpp:22,
                 from /usr/include/boost/range/concepts.hpp:19,
                 from /home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp:1:
/usr/include/c++/4.7/bits/range_access.h:97:5: Anmerkung: template<class _Tp, long unsigned int _Nm> _Tp* std::end(_Tp (&)[_Nm])
/usr/include/c++/4.7/bits/range_access.h:97:5: Anmerkung:   Herleitung/Ersetzung von Templateargument gescheitert:
/home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp:12:2: Anmerkung:   unpassende Typen »_Tp [_Nm]« und »const int«
In file included from /usr/include/c++/4.7/string:53:0,
                 from /usr/include/c++/4.7/bits/locale_classes.h:42,
                 from /usr/include/c++/4.7/bits/ios_base.h:43,
                 from /usr/include/c++/4.7/ios:43,
                 from /usr/include/c++/4.7/ostream:40,
                 from /usr/include/c++/4.7/iterator:64,
                 from /usr/include/boost/iterator.hpp:17,
                 from /usr/include/boost/concept_check.hpp:22,
                 from /usr/include/boost/range/concepts.hpp:19,
                 from /home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp:1:
/usr/include/c++/4.7/bits/range_access.h:78:5: Anmerkung: template<class _Container> decltype (__cont.end()) std::end(const _Container&)
/usr/include/c++/4.7/bits/range_access.h:78:5: Anmerkung:   Herleitung/Ersetzung von Templateargument gescheitert:
/usr/include/c++/4.7/bits/range_access.h: In Ersetzung von »template<class _Container> decltype (__cont.end()) std::end(const _Container&) [mit _Container = int]«:
/home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp:12:2:   erfordert durch »StreamT& operator<<(StreamT&, const ContainerT&) [mit ContainerT = int; StreamT = std::basic_stringstream<char>]«
/home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp:34:24:   von hier erfordert
/usr/include/c++/4.7/bits/range_access.h:78:5: Fehler: Abfrage des Elementes »end« in »__cont«, das vom Nicht-Klassentyp »const int« ist
/home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp: In Instanziierung von »StreamT& operator<<(StreamT&, const ContainerT&) [mit ContainerT = int; StreamT = std::basic_stringstream<char>]«:
/home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp:34:24:   von hier erfordert
/usr/include/c++/4.7/bits/range_access.h:68:5: Anmerkung: template<class _Container> decltype (__cont.end()) std::end(_Container&)
/usr/include/c++/4.7/bits/range_access.h:68:5: Anmerkung:   Herleitung/Ersetzung von Templateargument gescheitert:
/usr/include/c++/4.7/bits/range_access.h: In Ersetzung von »template<class _Container> decltype (__cont.end()) std::end(_Container&) [mit _Container = const int]«:
/home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp:12:2:   erfordert durch »StreamT& operator<<(StreamT&, const ContainerT&) [mit ContainerT = int; StreamT = std::basic_stringstream<char>]«
/home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp:34:24:   von hier erfordert
/usr/include/c++/4.7/bits/range_access.h:68:5: Fehler: Abfrage des Elementes »end« in »__cont«, das vom Nicht-Klassentyp »const int« ist
In file included from /usr/include/c++/4.7/utility:76:0,
                 from /usr/include/boost/config/no_tr1/utility.hpp:21,
                 from /usr/include/boost/config/select_stdlib_config.hpp:37,
                 from /usr/include/boost/config.hpp:40,
                 from /usr/include/boost/concept/assert.hpp:7,
                 from /usr/include/boost/concept_check.hpp:20,
                 from /usr/include/boost/range/concepts.hpp:19,
                 from /home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp:1:
/home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp: In Instanziierung von »StreamT& operator<<(StreamT&, const ContainerT&) [mit ContainerT = int; StreamT = std::basic_stringstream<char>]«:
/home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp:34:24:   von hier erfordert
/usr/include/c++/4.7/initializer_list:99:5: Anmerkung: template<class _Tp> constexpr const _Tp* std::end(std::initializer_list<_Tp>)
/usr/include/c++/4.7/initializer_list:99:5: Anmerkung:   Herleitung/Ersetzung von Templateargument gescheitert:
/home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp:12:2: Anmerkung:   unpassende Typen »std::initializer_list<_Tp>« und »int«
/home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp:12:2: Fehler: »const auto&« kann nicht aus »<Ausdrucksfehler>« hergeleitet werden
/home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp: In Instanziierung von »StreamT& operator<<(StreamT&, const ContainerT&) [mit ContainerT = SomeType; StreamT = std::basic_ostream<char>]«:
/home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp:49:15:   von hier erfordert
/home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp:12:2: Fehler: keine passende Funktion für Aufruf von »begin(const SomeType&)«
/home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp:12:2: Anmerkung: Kandidaten sind:
In file included from /usr/include/c++/4.7/string:53:0,
                 from /usr/include/c++/4.7/bits/locale_classes.h:42,
                 from /usr/include/c++/4.7/bits/ios_base.h:43,
                 from /usr/include/c++/4.7/ios:43,
                 from /usr/include/c++/4.7/ostream:40,
                 from /usr/include/c++/4.7/iterator:64,
                 from /usr/include/boost/iterator.hpp:17,
                 from /usr/include/boost/concept_check.hpp:22,
                 from /usr/include/boost/range/concepts.hpp:19,
                 from /home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp:1:
/usr/include/c++/4.7/bits/range_access.h:87:5: Anmerkung: template<class _Tp, long unsigned int _Nm> _Tp* std::begin(_Tp (&)[_Nm])
/usr/include/c++/4.7/bits/range_access.h:87:5: Anmerkung:   Herleitung/Ersetzung von Templateargument gescheitert:
/home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp:12:2: Anmerkung:   unpassende Typen »_Tp [_Nm]« und »const SomeType«
In file included from /usr/include/c++/4.7/string:53:0,
                 from /usr/include/c++/4.7/bits/locale_classes.h:42,
                 from /usr/include/c++/4.7/bits/ios_base.h:43,
                 from /usr/include/c++/4.7/ios:43,
                 from /usr/include/c++/4.7/ostream:40,
                 from /usr/include/c++/4.7/iterator:64,
                 from /usr/include/boost/iterator.hpp:17,
                 from /usr/include/boost/concept_check.hpp:22,
                 from /usr/include/boost/range/concepts.hpp:19,
                 from /home/richi/sourcecode/experiments/enable_if/enable_if_test.cpp:1:
/usr/include/c++/4.7/bits/range_access.h:58:5: Anmerkung: template<class _Container> decltype (__cont.begin()) std::begin(const _Container&)
/usr/include/c++/4.7/bits/range_access.h:58:5: Anmerkung:   Herleitung/Ersetzung von Templateargument gescheitert:

Wow, that was a mouth full. Because the overload types don’t match exactly, the template is considered, which in turn fails inside. What we need, is a way to tell the compiler that this template function should not be considered for types that aren’t containers. Too bad, the language has no direct support for that. So we are left with having to do some trickery. We could do this with tag dispatching and type traits. Tag dispatching is usually a technique to emulate partial template specialization for function templates. But for stuff like that, it’s quite helpful, if overly verbose and distracting. I wont show this technique here, as I think it’s better suited for different stuff. Instead I present the solution with enable_if. It works thanks to SFINAE (substitution failure is not an error). By not providing a valid type at all, the function is removed from the set of possible overloads.

#include <boost/range/concepts.hpp>
#include <type_traits>
#include <iostream>
#include <sstream>
#include <vector>
#include <list>
#include <iterator>

template <typename T>
struct sfinae_true : std::true_type {};

struct is_iterator_tester {
    template <typename T>
    static sfinae_true<typename std::iterator_traits<T>::iterator_category> test(int);

    template <typename>
    static std::false_type test(...);
};

template <typename T>
struct is_iterator : decltype(is_iterator_tester::test<T>(0)) {};

template<class ContainerT, class StreamT,
  typename enabler = typename std::enable_if<is_iterator<
    decltype(std::begin(*static_cast<ContainerT*>(nullptr)))>::value,
  ContainerT>::type>
StreamT& operator<<(StreamT& strm, const ContainerT& cont)
{
	strm << '{';
	for(const auto& element : cont)
		strm << element << ' ';
	strm << '}';

	return strm;
}

struct Base
{
    virtual std::string Describe() const = 0;
};

std::ostream& operator<<(std::ostream& strm, const Base& base)
{
    strm << base.Describe() << " ";
}

struct SomeType : public Base
{
    SomeType(int p1, int p2) : one_(p1), another_(p2) { }

    virtual std::string Describe() const
    {
        std::stringstream sstr;
        sstr << '{' << one_ << ' ' << another_ << '}';
        return sstr.str();
    }

	int one_;
	int another_;
};

int main()
{
	std::vector<int> ints{8, 45, 87, 90, 99999};
	std::list<float> floats{3.14159, 2.71828, 0.57721, 1.618033};
	std::cout << ints << floats;

	SomeType some{456, 789};
	std::cout << some;

	return 0;
}

You can find the full source code at github. I’m sure this could still be improved a lot. It’s not really easy to read. Probably boost::range::concepts could be of help. But I wanted an implementation which only uses the standard library. The is_iterator stuff was copied from Flaming Danger Zone.  I crafted the code for this blog post, and did not yet use it in production code. It would probably still need some tweaking.

For completeness, here is an implementation of enable_if itself. Considering all the above code, it is remarkably simple.

struct enable_if {}; // no members
template <bool Condition, typename T = void>

template <typename T>
struct enable_if<true, T> { using type = T; }; // type member

To conclude, I must admit, even the solution with enable_if could be a lot nicer. And there might be already something in the works.


Posted

in

,

by

Tags:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *