Невозможно передать std :: endl с перегруженным оператором <<() для std :: variant

В этом ответе описывается, как передавать автономный std::variant. Однако он не работает, когда std::variant хранится в std::unordered_map.

Следующий пример:

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

// /info/2437550/how-do-i-write-operator-for-stdvariant/6298838#6298838
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;
}

не удалось скомпилировать:

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,
                     ~~~~~~~~~~~~~~~~~~^~~

Почему это происходит? Как это можно исправить?

Ответ 1

В [temp.arg.explicit]/3 у нас есть это удивительное предложение:

Исходный набор параметров шаблонов шаблонов, не выведенный иначе, будет выведен на пустую последовательность аргументов шаблона.

Что это значит? Что такое набор шаблонов шаблонов? Что не выведено иначе? Это все хорошие вопросы, на которые на самом деле нет ответов. Но это имеет очень интересные последствия. Рассматривать:

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

Это... хорошо сформированный. Мы не можем вывести Ts... поэтому мы выводим его как пустую. Это оставляет нас с std::tuple<>, который является вполне допустимым типом - и вполне допустимым типом, который даже может быть создан с помощью {}. Так что это компилируется!

Итак, что происходит, когда вещь, которую мы выводим из пустого пакета параметров, который мы вызываем, не является допустимым типом? Вот пример:

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;
}

operator<< является потенциальным кандидатом, но вывод не подходит... или так кажется. Пока мы не соберем Ts... как пустые. Но Y<> является недопустимым типом! Мы даже не пытаемся выяснить, что мы не можем построить Y<> из std::endl - мы уже потерпели неудачу.

Это принципиально та же ситуация, что и у вас с variant, потому что variant<> не является допустимым типом.

Легкое решение состоит в том, чтобы просто изменить шаблон функции из variant<Ts...> в variant<T, Ts...>. Это больше не может вывести variant<>, что даже не возможно, поэтому у нас нет проблемы.

Ответ 2

По какой-то причине ваш код (который выглядит корректно для меня) пытается создать экземпляр std::variant<> (пустые альтернативы) как в clang, так и в gcc.

Обходной путь, который я нашел, - создать шаблон для специально непустого варианта. Поскольку std::variant не может быть пустым, я думаю, что обычно полезно писать общие функции для непустых вариантов.

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;
}

С этим изменением ваш код работает для меня.


Я также выяснил, что если std::variant имел специализацию std::variant<> без конструктора с одним аргументом, эта проблема не была бы в первую очередь. См. Первые строки в https://godbolt.org/z/VGih_4 и о том, как он работает.

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

Я делаю это, чтобы проиллюстрировать это, я не рекомендую это делать.

Ответ 3

Проблема в том, что std::endl но я озадачен, почему ваша перегрузка лучше соответствует сравнению с std :: basic_ostream :: operator <<, см. Пример livebolt live:

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

и удаление std::endl действительно устраняет проблему, см. ее в прямом эфире на Wandbox.

Поскольку alfC указывает, что изменение вашего оператора для запрета пустого варианта действительно устраняет проблему, см. Его в прямом эфире:

template<typename T, typename... Ts>
std::ostream& operator<<(std::ostream& os, const std::variant<T, Ts...>& v)