From SFINAE to Concepts

As software developers, our passion lies in harnessing the latest features of our favorite programming languages and frameworks. However, the harsh reality of daily work often imposes limitations, be it the environment, operating system, compiler, or underlying hardware. In the realm of long-term projects demanding continuous support and high stability, upgrading tools solely for the allure of fancy and modern features is not always a feasible option. The exception arises when critical weaknesses, safety concerns, or security issues necessitate an upgrade.

The C++20 standard (ISO/IEC 14882:2020(E) – Programming Language C++) brought forth several compelling features, including concepts, modules, and more. This is indeed great news for us, providing an opportunity to experiment with these advancements and potentially incorporate them into future commercial projects.

Let’s delve into a simple template function designed to process any type of data:

template <typename T>
void process(T) {}

But what if we wish to narrow down this function to operate exclusively on integral types?

template <typename T, typename = std::enable_if_t<std::is_integral_v<T>>>
void process(T) { /* handles integral types only */ }

With the advent of C++20 concepts, we can introduce a more readable alternative:

template <typename T>
concept Integral = std::is_integral_v<T>;

template <Integral T>
void process(T) { /* handles integral types only */ }

Moreover, we can define multiple concepts and leave a concept-less base template to handle any other types lacking a specific implementation:

template <typename T>
concept Integral = std::is_integral_v<T>;

template <typename T>
concept Floating_point = std::is_floating_point_v<T>;

template <Integral T>
void process(T) { /* handles integral types only */ }

template <Floating_point T>
void process(T) { /* handles floating types only */}

template <typename T>
void process(T) { /* handles any other types */ }

While these examples are simplistic, envision the need to handle container-like iterable objects. Using SFINAE, we can implement a template function with such a limitation:

template <typename IterT, typename ContainerT>
using Container_iterator = typename std::enable_if_t<std::same_as<IterT, typename ContainerT::iterator> || std::same_as<IterT, typename ContainerT::const_iterator>>;

template <typename IterT, typename ContainerT, typename = void>
struct is_container_iterator : std::false_type {};

template <typename IterT, typename ContainerT>
struct is_container_iterator<IterT, ContainerT, Container_iterator<IterT, ContainerT>> : std::true_type {};

template <typename IterT, typename ContainerT>
constexpr bool is_container_iterator_v = is_container_iterator<IterT, ContainerT>::value;

template <typename T>
using Container = typename std::enable_if_t<is_container_iterator_v<decltype(std::declval<T &>().begin()), T> &&
                                            is_container_iterator_v<decltype(std::declval<T &>().end()), T> &&
                                            is_container_iterator_v<decltype(std::declval<T &>().cbegin()), T> &&
                                            is_container_iterator_v<decltype(std::declval<T &>().cend()), T> &&
                                            std::same_as<decltype(std::declval<T &>().size()), typename T::size_type>>;

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

template <typename T>
struct is_container<T, Container<T>> : std::true_type {};

template <typename T>
constexpr bool is_container_v = is_container<T>::value;

template <typename T, typename = std::enable_if_t<is_container_v<T>>>
void process(T) { /* handles container-like types */ }

Now, let’s reimagine the same functionality using concepts:

template <typename IterT, typename ContainerT>
concept Container_iterator =
    std::same_as<IterT, typename ContainerT::iterator> ||
    std::same_as<IterT, typename ContainerT::const_iterator>;

template <typename T>
concept Container = requires(T cont) {
    {
        cont.begin()
    } -> Container_iterator<T>;
    {
        cont.end()
    } -> Container_iterator<T>;
    {
        cont.cbegin()
    } -> Container_iterator<T>;
    {
        cont.cend()
    } -> Container_iterator<T>;
    {
        cont.size()
    } -> std::same_as<typename T::size_type>;
};

template <Container C>
void process(T) { /* handles container-like types */ }

The difference is evident – concepts not only simplify the life of those implementing them but, more importantly, enhance the readability and maintainability of the code for those who come across it later.

From SFINAE to Concepts

Leave a Reply

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

Scroll to top