Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ expression SFINAE and ostream manipulators

Tags:

I am trying to learn how to use SFINAE. For practice purposes, I was trying to make an std::ostream wrapper in order to make a custom formatter.

Here is my SFINAE and custom output class.

// Tester
template <class O>
struct is_ostreamable {
    template <class T>
    static auto check(T t) -> decltype(std::declval<std::ostream &>() << t, std::true_type());
    template <class>
    static auto check(...) -> std::false_type;

  public:
    static constexpr bool value{std::is_same_v<decltype(check<O>(0)), std::true_type>};
};

// Custom class
struct CustomOutput {
    // Constructor etc...
    CustomOutput(std::ostream &os = std::cout) : os{os} {}
    std::ostream &os;

    // Problematic template function
    template <class O, class = std::enable_if_t<is_ostreamable<O>::value>>
    CustomOutput &operator<<(O o) {
        os << o;
        return *this;
    }
};

It words perfectly to not enable the template for struct or class that cannot be printed via operator<<. However, with this SFINAE, ostream manipulators are not working... And I can't figure out why.

The error, and my expectations:

int main(void){
    CustomOutput{} << "hi"; // Fine

    std::vector<int> vec;
    // CustomOutput{} << vec; // Error. Expected

    CustomOutput{} << std::endl; // Error. WHY?
}

Maybe I missed something? Any help would be greatly appreciated.

like image 672
funnypig run Avatar asked Jan 30 '20 06:01

funnypig run


2 Answers

First, fix your ostreamable class. Currently, your class requires T to be copy-constructible from 0. This is not the case for many classes. It should use std::declval to create the value instead:

template <class O>
struct is_ostreamable {
    template <class T>
    static auto check(int) -> decltype(std::declval<std::ostream &>() << std::declval<T>(), std::true_type());
    template <class>
    static auto check(...) -> std::false_type;

public:
    static constexpr bool value{std::is_same_v<decltype(check<O>(0)), std::true_type>};
};

Two changes are made here:

  • The operand to decltype uses std::declval<T>() to create the object of type T. std::declval<T> is an (intentionally undefined) function template that generates an object of type T when used in an unevaluated operand (such as that to decltype, or sizeof, noexcept operator, etc.) without dependence on a specific construction signature (copy-construction from 0 in your case).

  • The parameter to check is replaced with int. The initializer of the value variable calls check with the argument 0, so this int parameter ensures that (int) ranks higher than (...) in overload resolution, so that the true_type overload gets chosen when possible.


You need to provide a special overload for function-style manipulators (std::endl, std::flush, etc.):

using manip = std::ostream& (*)(std::ostream&);

CustomOutput& operator<<(manip m) {
    os << m;
    return *this;
}

There is, unfortunately, no way to make the generic template version support this feature. This is because std::endl is a function template:

template <class CharT, class Traits>
std::basic_ostream<CharT, Traits>& endl(td::basic_ostream<CharT, Traits>& os);

For a function template to be used, the appropriate template arguments have to be determined. It is not possible to deduce the type-template parameter T as a generic template.

Anyway, this is probably the only special overload you are going to need.

like image 60
L. F. Avatar answered Nov 15 '22 06:11

L. F.


I know there is already an accepted answer but I would like to mention a bit prettier C++20 way of doing the same thing by using concepts:

#include <iostream>
#include <concepts>

using OManipulator= std::ostream&(&)(std::ostream &);

template <typename T>
concept OStreamable = requires(T t) {
    std::declval<std::ostream&>() << t;
};

struct CustomOutput {
    std::ostream &os;

    CustomOutput(std::ostream &os = std::cout)
        : os{os}
    {}

    template <typename T> requires OStreamable<T>
    CustomOutput& operator<<(T out) {
        os << out;
        return *this;
    }

    CustomOutput& operator<<(OManipulator out) {
        os << out;
        return *this;
    }
};

int main(void){
    CustomOutput{} << "hello";
    CustomOutput{} << std::endl;
    CustomOutput{} << "world";
}

Basically, in C++20, problem with manipulators needs to be solved in the same way as pre-C++20, by providing a special overload for them.

like image 24
NutCracker Avatar answered Nov 15 '22 07:11

NutCracker