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?