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