Search code examples
c++variantvisitor-pattern

Calling << operator on types held in a std::variant?


I've got a struct like this:

// Literal.hpp
struct Literal
{
    std::variant<
        std::nullptr_t,
        std::string,
        double,
        bool
        >
        value;

    friend std::ostream &operator<<(std::ostream &os, Literal &literal);
};

and I'm trying to implement the << operator like this:

// Literal.cpp
Literal::Literal() : value(value) {}

std::ostream &operator<<(std::ostream &os, const Literal &literal)
{
    std::visit(/* I don't know what to put here!*/, literal.value);
}

I've tried implementing the operator like this (note: I would take any elegant solution it doesn't have to be a solution to this implementation below)

// In Literal.cpp
std::ostream &operator<<(std::ostream &out, const Literal literal)
{
    std::visit(ToString(), literal.value);
    return out;
}

struct ToString; // this declaration is in literal.hpp

void ToString::operator()(const std::nullptr_t &literalValue){std::cout << "null";}
void ToString::operator()(const char &literalValue){std::cout << std::string(literalValue);}
void ToString::operator()(const std::string &literalValue){std::cout << literalValue;}
void ToString::operator()(const double &literalValue){std::cout << literalValue;}
void ToString::operator()(const bool &literalValue){std::cout << literalValue;}

But in my main function, passing a char array literal doesn't casts it into a bool when it runs! ignoring the operator overload taking a char:

main() {
    Literal myLiteral;
    myLiteral.value = "Hello World";
    std::cout << myLiteral << std::endl;
}

Solution

  • This is a bug in your standard library. Presumably you're using libstc++ (the GNU C++ standard library), since that's what Godbolt shows as messing up. If you compile with libc++ (Clang/LLVM's C++ standard library), this works as expected. According to std::vector<Types...>::operator=(T&& t)'s cppreference page, it

    Determines the alternative type T_j that would be selected by overload resolution for the expression F(std::forward<T>(t)) if there was an overload of imaginary function F(T_i) for every T_i from Types... in scope at the same time, except that:

    • An overload F(T_i) is only considered if the declaration T_i x[] = { std::forward<T>(t) }; is valid for some invented variable x;

    • If T_i is (possibly cv-qualified) bool, F(T_i) is only considered if std:remove_cvref_t<T> is also bool.

    That last clause is there for this very situation. Because lots of things can convert to bool, but we don't usually intend this conversion, that clause causes conversion sequences that would not normally be selected to be selected (char const* to bool is a standard conversion, but to std::string is "user-defined", which is normally considered "worse"). Your code should set value to its std::string alternative, but your library's implementation of std::variant is broken. There's probably an issue ticket already opened, but if there isn't, this is grounds to open one. If you're stuck with your library, explicitly marking the literal as a std::string should work:

    literal.value = std::string("Hello World");
    

    For the elegance question, use an abbreviated template lambda.

    std::ostream &operator<<(std::ostream &os, Literal const &literal)
    {
        std::visit([](auto v) { std::cout << v; }, literal.value);
        // or
        std::visit([](auto const &v) {
            // gets template param      vvvvvvvvvvvvvvvvvvvvvvvvv w/o being able to name it
            if constexpr(std::is_same_v<std::decay_t<decltype(v)>, std::nullptr_t>) {
               std::cout << "null";
            } else std::cout << v;
        }, literal.value);
        // only difference is nullptr_t => "nullptr" vs "null"
    
        return std::cout;
    }
    

    Also, your friend declaration doesn't match the definition. Actually, it shouldn't be friended anyway, since it needs no access to private members.

    // declaration in header, outside of any class, as a free function
    std::ostream &operator<<(std::ostream&, Literal const&);
    //                            was missing const ^^^^^