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 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?