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://stackoverflow.com/a/46893057/8414561
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?

Dev Null
  • 4,731
  • 1
  • 30
  • 46
  • 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&))`. – David G Oct 17 '18 at 01:09
  • 1
    You might simplify the sample: http://coliru.stacked-crooked.com/a/5409eb3df588e395 – sehe Oct 17 '18 at 09:10
  • @Loreto that `__cdecl` isn't going to help the OP's compiler (GCC) decide anything. – Jonathan Wakely Oct 18 '18 at 12:12

3 Answers3

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.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • 2
    Or `,std::enable_if_t< 0 = true>` SFINAE. Because typing `T0, Ts...` is annoying. ;) – Yakk - Adam Nevraumont Oct 17 '18 at 01:59
  • 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? – alfC Oct 17 '18 at 02:10
  • @alfC I honestly view it as more of a language bug than a library bug – Barry Oct 17 '18 at 02:18
  • @alfC [It can't](https://timsong-cpp.github.io/cppwp/variant.variant#3) – Passer By Oct 17 '18 at 02:23
  • 1
    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. – Shafik Yaghmour Oct 17 '18 at 02:30
  • 2
    @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. – Barry Oct 17 '18 at 02:52
  • 2
    @Barry I am sure it is a compiler bug, see my answer. I don't know if the standard could be clearer about that. – Oliv Oct 17 '18 at 08:22
  • @Oliv I really don't think so - and either way, it pretty obviously could be clearer. – Barry Oct 17 '18 at 14:26
  • @Barry So it is unclear! I still think what I wrote too. – Oliv Oct 17 '18 at 15:03
  • @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 https://godbolt.org/z/VGih_4 – alfC Oct 17 '18 at 23:46
  • BTW, adding a specialization like this is what would make `variant` SFINAE friendly: `namespace std{ template<> struct variant<>{}; }`. – alfC Oct 17 '18 at 23:48
  • 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]. – aschepler Oct 17 '18 at 23:56
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.

alfC
  • 14,261
  • 4
  • 67
  • 118
  • isn't specialising `std::variant` _in `namespace std`_ UB? – Dev Null Oct 18 '18 at 12:04
  • @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. – alfC Oct 18 '18 at 12:50
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)
Shafik Yaghmour
  • 154,301
  • 39
  • 440
  • 740
  • 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`. – alfC Oct 17 '18 at 01:27
  • @alfC the wandbox example I link to shows it working w/o the endl. – Shafik Yaghmour Oct 17 '18 at 01:27
  • 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`? – alfC Oct 17 '18 at 01:33