Can't stream std::endl with overloaded operator<<() for std::variant
Asked Answered
T

3

26

This answer describes how to stream a standalone std::variant. However, it doesn't seem to work when std::variant is stored in a std::unordered_map.

The following example:

#include <iostream>
#include <string>
#include <variant>
#include <complex>
#include <unordered_map>

// https://mcmap.net/q/536438/-how-do-i-write-operator-lt-lt-for-std-variant
template<typename... Ts>
std::ostream& operator<<(std::ostream& os, const std::variant<Ts...>& v)
{
    std::visit([&os](auto&& arg) {
        os << arg;
    }, v);
    return os;
}

int main()
{
    using namespace std::complex_literals;
    std::unordered_map<int, std::variant<int, std::string, double, std::complex<double>>> map{
        {0, 4},
        {1, "hello"},
        {2, 3.14},
        {3, 2. + 3i}
    };

    for (const auto& [key, value] : map)
        std::cout << key << "=" << value << std::endl;
}

fails to compile with:

In file included from main.cpp:3:
/usr/local/include/c++/8.1.0/variant: In instantiation of 'constexpr const bool std::__detail::__variant::_Traits<>::_S_default_ctor':
/usr/local/include/c++/8.1.0/variant:1038:11:   required from 'class std::variant<>'
main.cpp:27:50:   required from here
/usr/local/include/c++/8.1.0/variant:300:4: error: invalid use of incomplete type 'struct std::__detail::__variant::_Nth_type<0>'
    is_default_constructible_v<typename _Nth_type<0, _Types...>::type>;
    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/local/include/c++/8.1.0/variant:58:12: note: declaration of 'struct std::__detail::__variant::_Nth_type<0>'
     struct _Nth_type;
            ^~~~~~~~~
/usr/local/include/c++/8.1.0/variant: In instantiation of 'class std::variant<>':
main.cpp:27:50:   required from here
/usr/local/include/c++/8.1.0/variant:1051:39: error: static assertion failed: variant must have at least one alternative
       static_assert(sizeof...(_Types) > 0,
                     ~~~~~~~~~~~~~~~~~~^~~

Why does it happen? How is it possible to fix it?

Triley answered 17/10, 2018 at 0:15 Comment(3)
For whatever reason your overload of << is a better match for std::cout << std::endl than the standard std::osteam& std::operator<<(std::ostream&, std::ostream&(*pf)(std::ostream&)).Incumbent
You might simplify the sample: coliru.stacked-crooked.com/a/5409eb3df588e395Czardas
@Loreto that __cdecl isn't going to help the OP's compiler (GCC) decide anything.Vitia
T
34

In [temp.arg.explicit]/3, we have this amazing sentence:

A trailing template parameter pack not otherwise deduced will be deduced to an empty sequence of template arguments.

What does this mean? What is a trailing template parameter pack? What does not otherwise deduced mean? These are all good questions that don't really have answers. But this has very interesting consequences. Consider:

template <typename... Ts> void f(std::tuple<Ts...>);
f({}); // ok??

This is... well-formed. We can't deduce Ts... so we deduce it as empty. That leaves us with std::tuple<>, which is a perfectly valid type - and a perfectly valid type that can even be instantiated with {}. So this compiles!

So what happens when the thing we deduce from the empty parameter pack we conjured up isn't a valid type? Here's an example:

template <class... Ts>
struct Y
{
    static_assert(sizeof...(Ts)>0, "!");
};


template <class... Ts>
std::ostream& operator<<(std::ostream& os, Y<Ts...> const& )
{
    return os << std::endl;
}

The operator<< is a potential candidate, but deduction fails... or so it would seem. Until we conjure up Ts... as empty. But Y<> is an invalid type! We don't even try to find out that we can't construct a Y<> from std::endl - we have already failed.

This is fundamentally the same situation you have with variant, because variant<> is not a valid type.

The easy fix is to simply change your function template from taking a variant<Ts...> to a variant<T, Ts...>. This can no longer deduce to variant<>, which isn't even a possible thing, so we don't have a problem.

Topless answered 17/10, 2018 at 1:29 Comment(13)
Or ,std::enable_if_t< 0<sizeof...(Ts), bool> = true> SFINAE. Because typing T0, Ts... is annoying. ;)Millard
Excellent catch. Even seems like a useful feature (except when it catches you off guard). Can't variant<> by implemented in a SFINAE friendly way?Brote
@Brote I honestly view it as more of a language bug than a library bugTopless
@Brote It can'tElsieelsinore
I feel like this answer raised more questions then it answers, if you attempt this with other types it does not fail in this way. Why it is attempting this deduction for std::endl but not other cases? or if it is attempting the deduction in other cases then why is it valid for other cases? I have to wonder if this is really the intended behavior, it feesl like a very surprising trap.Vigesimal
@Shafik Because endl is a function template. So, like the {} example, it's not a thing with a type. So deduction fails and we fall back to empty Ts... I very much agree it is a surprising trap.Topless
@Topless I am sure it is a compiler bug, see my answer. I don't know if the standard could be clearer about that.Cabob
Gotta love C++.Aesculapian
@Cabob I really don't think so - and either way, it pretty obviously could be clearer.Topless
@Topless So it is unclear! I still think what I wrote too.Cabob
@Barry, looking closely I think this is a bug of the language, the compiler and the library implementation all at the same time, take a look at godbolt.org/z/VGih_4Brote
BTW, adding a specialization like this is what would make variant SFINAE friendly: namespace std{ template<> struct variant<>{}; }.Brote
I think at least the question "What does not otherwise deduced mean?" has a very clear answer: the template parameter pack is not deduced by any of the rules in [temp.deduct].Mailer
B
7

For some reason, your code (which looks correct to me) is trying to instantiate std::variant<> (empty alternatives) both in clang and gcc.

The workaround I found is to make a template for a specifically non-empty variant. Since std::variant cannot be empty anyway, I think it is usually good to write generic functions for non-empty variants.

template<typename T, typename... Ts>
std::ostream& operator<<(std::ostream& os, const std::variant<T, Ts...>& v)
{
    std::visit([&os](auto&& arg) {
        os << arg;
    }, v);
    return os;
}

With this change, your code works for me.


I also figured out that if std::variant had a specialization of std::variant<> without a single-argument constructor, this problem would not have happened in the first place. See the first lines in https://godbolt.org/z/VGih_4 and how it makes it work.

namespace std{
   template<> struct variant<>{ ... no single-argument constructor, optionally add static assert code ... };
}

I am doing this just to illustrate the point, I don't necessarely recommend doing this.

Brote answered 17/10, 2018 at 1:23 Comment(2)
isn't specialising std::variant in namespace std UB?Triley
@DevNull, yes and I don't recommend doing it, it is just to show that variant could be implemented in a slightly different way to avoid this pitfall. Meaning that something can be done at the implementation level.Brote
V
5

The problem is the std::endl but I am puzzled why your overload is a better match than the one from std::basic_ostream::operator<<, see godbolt live example:

<source>:29:12: note: in instantiation of template class 'std::variant<>' requested here
        << std::endl;
           ^

and removing the std::endl indeed fixes the problem, see it live on Wandbox.

As alfC points out altering your operator to disallow an empty variant does indeed fix the issue, see it live:

template<typename T, typename... Ts>
std::ostream& operator<<(std::ostream& os, const std::variant<T, Ts...>& v)
Vigesimal answered 17/10, 2018 at 0:49 Comment(3)
In my experiment, the code fails whether you have the std::endl or not. (and whether you have the "="). gcc 8.1/clang 6.0.Brote
@Brote the wandbox example I link to shows it working w/o the endl.Vigesimal
Ah, yes you are right. (I have a std::cout << std::endl somewhere else in the test code). It looks as if std::variant<> has an implicit contructor from anything?, maybe it is a bug in the implementation of std::variant?Brote

© 2022 - 2024 — McMap. All rights reserved.