Templates in C++ primer (4)

In this article we’re going to look at an application of Substitution Failure Is Not An Error (SFINAE). The use case described actually uses SFINAE twice, firstly to create a traits template very similar to the one already seen in this mini-series, and secondly to switch a function template instantiation on or off.

So let’s first define the problem. Other languages allow a class to define a .toString() method (or similarly named, Python uses .__str__()) in order to allow output of objects of that class. As a starting point we might try to define a template function along the lines of:

template<typename T>
ostream& operator<< (ostream& os, const T& obj) {
    return os << obj.to_string();
}

This naïve approach might work depending on your library implementation and code structure; problems arise when (as you might guess) an ambiguity occurs as to whether template instantiation should be attempted.

So we need to do two things at compile time: determine whether T::to_string() exists, and based on this result switch generation of operator<< for T on or off. In Modern C++ the first of these is easy enough and the code is presented below as a pair of class templates, the second of which is partially specialized:

template<typename, typename = std::void_t<>>
struct HasMemberT_toString : false_type{};

template<typename T>
struct HasMemberT_toString<T, void_t<decltype(declval<T>().to_string())>> : true_type{};

We can now check if MyClass has a member function to_string() with the following:

static_assert(HasMemberT_toString<MyClass>::value);

(Note: the use of ::value is necessary here; this data member is inherited from either std::false_type or std::true_type.)

Back to the definition of operator <<, we need to switch it on or off with std::enable_if. This is a bit of template “magic” that takes a boolean as the first template (non-type) parameter, and also a second (type) parameter which is the return type of the function. Shown below is the modified version, assumed defined with HasMemberT_toString<> available:

template<typename T>
enable_if<HasMemberT_toString<T>::value, ostream&>::type
operator<< (ostream& os, const T& obj) {
    return os << obj.to_string();
}

Again, the use of ::type is necessary to extract either ostream& or a “special” null type from std::enable_if (the second of which causes instantiation to not be attempted).

So there we have it, a compile-time selected operator<< which should never produce screenfuls of error messages if you accidentally try to send a class without to_string() to std::cout, or similar, whilst automatically giving all classes which provide this member function compatibility with (output) streams.

Below is a complete program with an example Book class encapsulating a title and author(s):

// sfinae_tostring.cpp : use of SFINAE with traits and enable_if 

#include <type_traits>
#include <iostream>
#include <string>
#include <initializer_list>
#include <vector>

using namespace std;

class Book {
    string d_title;
    vector<string> d_authors;
public:
    Book(const string& title, const initializer_list<string>& authors)
        : d_title{ title }, d_authors{ authors }
    {}
    string to_string() const {
        string s{}, sep{};
        for (const auto& a : d_authors) {
            s.append(sep);
            s.append(a);
            sep = ", "s;
        }
        s.append(": ");
        s.append(d_title);
        return s;
    }
};

template<typename, typename = std::void_t<>>
struct HasMemberT_toString : false_type{};

template<typename T>
struct HasMemberT_toString<T, void_t<decltype(declval<T>().to_string())>> : true_type{};

static_assert(HasMemberT_toString<Book>::value);

template<typename T>
enable_if<HasMemberT_toString<T>::value, ostream&>::type
operator<< (ostream& os, const T& obj) {
    return os << obj.to_string();
}

int main() {
    Book book1{ "C++ Annotations", { "Frank B. Brokken" } },
        book2{ "C++ Templates", { "David Vandevoorde", "Nicolai M. Josuttis", "Douglas Gregor" } };
    cout << book1 << '\n' << book2 << '\n';
}

That wraps up this mini-series, and I hope you’re now interested by some of the possibilities provided by the availablity of templates in Modern C++.

Resources

  • David Vandevoorde, Nicolai M. Josuttis, Douglas Gregor C++ Templates, The Complete Guide, Second Edition (Pearson, 2018, ISBN13: 9780321714121)
  • Frank B. Brokken’s C++ Annotations available here has extensive coverage of generics, in particular see the chapter on “Advanced Template Use”
  • Download or browse the source code from this mini-series

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s