Code Snippets
/

Template Constraints with C++20 Concepts (and SFINAE Fallback)

Template Constraints with C++20 Concepts (and SFINAE Fallback)

C++20 concepts (`requires` clauses) replace decades of SFINAE incantations with readable predicate-style template constraints. The runnable accordions in this entry use the C++17 `std::enable_if_t` SFINAE form because the test compiler is GCC 9.2 (which lacks concepts), and the C++20 equivalent is shown in inline comments. Reach for concepts on any compiler that supports them; reach for SFINAE only when constrained by toolchain age.

C++
Hard
cpp-templates
cpp-metaprogramming
type-system
generics

870 views

16

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

// C++20:
//   template <typename T>
//   concept Printable = requires(std::ostream& os, const T& t) { os << t; };
//
//   template <Printable T> void show(const T& t) { std::cout << t << "\n"; }
//
// C++17 SFINAE equivalent: detect whether `os << t` is a valid expression via
// std::declval inside a void_t alias, then enable the overload only when it is.

template <typename, typename = void>
struct is_printable : std::false_type {};

template <typename T>
struct is_printable<T, std::void_t<decltype(std::declval<std::ostream&>() << std::declval<const T&>())>>
    : std::true_type {};

template <typename T>
typename std::enable_if<is_printable<T>::value>::type
show(const T& t) {
    std::cout << t << "\n";
}

struct NotPrintable { int x; };

int main() {
    show(42);
    show(std::string("hello"));
    show(3.14);
    // show(NotPrintable{1});  // would not compile: no matching overload.
    std::cout << "is_printable<int>="           << is_printable<int>::value           << "\n";
    std::cout << "is_printable<NotPrintable>="  << is_printable<NotPrintable>::value  << "\n";
    return 0;
}

The C++20 form uses concept Printable = requires(std::ostream& os, const T& t) { os << t; }; and reads exactly like a predicate: "T is Printable when os << t compiles". The C++17 SFINAE form here achieves the same compile-time test through three pieces: a primary trait that defaults to false_type, a partial specialisation that activates only when decltype(os << t) is well-formed (gated by std::void_t), and an enable_if on the function template that excludes the overload when the trait is false. The result is identical: show(NotPrintable{}) is a hard compile error pointing at "no matching overload" rather than a 200-line template-stack barf. Concepts are easier to write, easier to read, and produce vastly better error messages.

2 more snippets in this entry are available for premium members.

Upgrade to Premium