Quantcast
Channel: Using SFINAE to provide "default" output operators - Code Review Stack Exchange
Viewing all articles
Browse latest Browse all 2

Using SFINAE to provide "default" output operators

$
0
0

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)? In this case I have to provide output operators for the types, so the framework is able to print the values in a "default" way. However, a default output operator should not be called for some type T if the type already has output operator.

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 nottemplate<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 nottemplate<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 operatorstemplate <typename T>typename std::enable_if<is_iteratable<T>::value &&                        !has_output_operator<T>::value,        std::ostream&>::typeoperator << (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&>::typeoperator << (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&>::typeoperator << (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 versionproject(SFINAE VERSION 0.1)# specify the C++ standardset(CMAKE_CXX_STANDARD 17)set(CMAKE_CXX_STANDARD_REQUIRED True)# set a variable for .cpp source filesset(SOURCES        main.cpp)# set a variable .h headersset(HEADERS        sfinae_print.hpp)# add the executableadd_executable(SFINAE ${SOURCES} ${HEADERS})# enable all warnings during compile process# append flag to previously defined flagset(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?


Viewing all articles
Browse latest Browse all 2

Latest Images

Trending Articles





Latest Images