c++ – Using SFINAE to provide “default” output operators


I am an undergraduate CS student trying to implement simple unit test framework on C++ as a pet project.

The framework has an assertion macro like ASSERT_EQ(var1, var2), which checks whether two variables are equal or not. If the assertion fails, I want to print a failure message to std::cerr and provide as much information as possible about failed comparison. Therefore, values var1 and var2 are to be printed. But what if there is no corresponding operator << for the variables’ type(s)?

With a relative success, I employed SFINAE for it in the following way.

file “sfinae_print.hpp”:

#include <type_traits> // for enable_if
#include <iostream>
#include <utility>

//////////////////////////////////////
//   META TYPE CHECK: OPERATOR <<   //
//////////////////////////////////////

// checks whether T has declared output iterator or not
template<typename T>
class has_output_operator
{
private:

    // decltype(...) statically evaluates the type of passed expression
    //      fail leads to substitution failure in the context
    template<typename U, typename = decltype(std::cout << std::declval<U>())>
    static constexpr bool
    check(nullptr_t) noexcept
    {
        return true;
    }

    // less specialized function - check(nullptr_t) is
    //      more preferable (but may cause substitution failure)
    template<typename ...>
    static constexpr bool check(...) noexcept
    {
        return false;
    }

public:
    static constexpr bool value{check<T>(nullptr)};
};

///////////////////////////////////////
//   META TYPE CHECK: IS ITERATABLE  //
///////////////////////////////////////

// checks whether T is iteratable (supports begin() and end()) or not
template<typename T>
class is_iteratable
{
private:

    template<typename U>
    static constexpr decltype(std::begin(std::declval<U>()),
            std::end(std::declval<U>()),
            bool())
    check(nullptr_t) noexcept
    {
        return true;
    }

    template<typename ...>
    static constexpr bool check(...) noexcept
    {
        return false;
    }

public:
    static constexpr bool value{check<T>(nullptr)};
};

template<typename T>
void print_meta_info(std::ostream& os = std::cout)
{
    os << "is iteratable: " << is_iteratable<T>::value << std::endl;
    os << "has output operator: " << has_output_operator<T>::value << std::endl;
    os << std::endl;
}

/////////////////////////
//   ITERATABLE TYPE   //
/////////////////////////

// "default" output operators:

// operator << for iteratable type with no other output operators
template <typename T>
typename std::enable_if<is_iteratable<T>::value &&
                        !has_output_operator<T>::value,
        std::ostream&>::type
operator << (std::ostream& os, const T& obj)
{
    bool flag{false};

    os << "{";
    for(const auto& unit : obj)
    {
        if(flag)
        {
            os << ", ";
        }
        flag = true;
        os << unit;
    }
    os << "}";

    return os;
}

//////////////
//   PAIR   //
//////////////
// same for a pair:
template <typename LHS, typename RHS>
typename std::enable_if<!has_output_operator<std::pair<LHS, RHS>>::value, std::ostream&>::type
operator << (std::ostream& os, const std::pair<LHS, RHS>& obj)
{
    return os << "{" << obj.first << "," << obj.second << "}";
}

/////////////////////////////
//   NON-ITERATABLE TYPE   //
/////////////////////////////
// same for non-iteratable type:
template <typename T>
typename std::enable_if<!is_iteratable<T>::value &&
                        !has_output_operator<T>::value,
        std::ostream&>::type
operator << (std::ostream& os, const T& value)
{
    // cannot be printed
    // some failure is bound to occur
    return os << value;
    // print POD with reflection?
}

Usage example:

#include <iostream>
#include <vector>
#include <string>
#include <map>
#include <set>

#include "sfinae_print.hpp"

std::ostream& operator << (std::ostream& os, const std::set<int> obj)
{
    return os << "explicitly defined operator << for set<int>";
}

int main()
{
    // printing values using "default" output operators: 
    const std::vector<std::string> v_str{"sfinae", "is", "dope"};
    std::cout << v_str << std::endl;

    const std::vector<int> v_int{1, 2, 3};
    std::cout << v_int << std::endl;

    const std::map<int, int> map_int_int{{1, 2}, {3, 4}};
    std::cout << map_int_int << std::endl;

    // std::set<int> has defined operator <<, thus there is no need in default operator << 
    const std::set<int> set_int{1, 9, 1, 7};
    std::cout << set_int << std::endl;

    return 0;
}

CMakeLists.txt:

cmake_minimum_required(VERSION 3.17)

# set the project name and version
project(SFINAE VERSION 0.1)

# specify the C++ standard
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)

# set a variable for .cpp source files
set(SOURCES
        main.cpp
)

# set a variable .h headers
set(HEADERS
        sfinae_print.hpp
)

# add the executable
add_executable(SFINAE ${SOURCES} ${HEADERS})

# enable all warnings during compile process
# append flag to previously defined flag
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall  -Wextra")

Is it a tolerable solution? Could you please give me any suggestions on how to improve it?