无法使用重载的operator<<()为std::variant流式传输std::endl

26

这个答案描述了如何流式传输一个独立的std::variant。但是,当 std::variant 存储在 std::unordered_map 中时,它似乎无法工作。

以下是示例:

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

// https://dev59.com/cabja4cB1Zd3GeqPcS0B#46893057
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,
                     ~~~~~~~~~~~~~~~~~~^~~

为什么会发生这种情况?如何修复它?


无论出于何种原因,您重载的<<比标准的std::osteam& std::operator<<(std::ostream&, std::ostream&(*pf)(std::ostream&))更适合于std::cout << std::endl - David G
1
你可以简化这个示例:http://coliru.stacked-crooked.com/a/5409eb3df588e395 - sehe
@Loreto,__cdecl 对 OP 的编译器(GCC)没有任何帮助。 - Jonathan Wakely
3个回答

34
[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<>是一个无效的类型!我们甚至没有尝试去发现我们不能从std::endl构造一个Y<> - 我们已经失败了。

这与variant面临的情况基本相同,因为variant<>不是一个有效的类型。

简单的解决方法是将函数模板从接受variant<Ts...>更改为接受variant<T, Ts...>。这不再能够推导出variant<>,这甚至不是一件可能的事情,所以我们没有问题。


2
或者使用 std::enable_if_t< 0<sizeof...(Ts), bool> = true> SFINAE。因为输入 T0, Ts... 太麻烦了。 ;) - Yakk - Adam Nevraumont
1
我觉得这个答案引发了更多问题,如果你尝试使用其他类型,它不会以这种方式失败。为什么它会针对 std::endl 尝试这种推断,而其他情况却不是呢?或者如果它在其他情况下尝试推断,那么为什么其他情况下它是有效的呢?我不得不怀疑这是否真的是预期的行为,它感觉像一个非常令人惊讶的陷阱。 - Shafik Yaghmour
2
@Shafik 因为 endl 是一个函数模板。所以,就像 {} 的例子一样,它不是一个具有类型的东西。因此,推导失败,我们回到了空的 Ts...。我非常同意这是一个令人惊讶的陷阱。 - Barry
2
@Barry 我确定这是编译器的错误,请看我的回答。我不知道标准是否可以更清晰地说明这一点。 - Oliv
顺便提一下,像这样添加一个特化将使 variant 成为 SFINAE 友好的:namespace std{ template<> struct variant<>{}; } - alfC
显示剩余8条评论

7
由于某些原因,您的代码(在我看来是正确的)尝试在clang和gcc中实例化std::variant<>(空替代),我找到的解决方法是为特定的非空变体制作模板。由于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 ... };
}

我只是为了说明问题而这样做,不一定推荐这样做。


namespace std 中专门使用 std::variant 不是未定义行为吗? - Dev Null
@DevNull,是的,我不建议这样做,只是为了展示变体可以以稍微不同的方式实现以避免这个陷阱。这意味着在实现层面上可以做些什么。 - alfC

5
问题出在std::endl上,但我很困惑为什么你的重载比std::basic_ostream::operator<<更匹配,可以参考godbolt实时例子
<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)

在我的实验中,无论你是否使用std::endl或者是否有"=",代码都会失败。使用的编译器为gcc 8.1/clang 6.0 - alfC
@alfC,我提供的wandbox示例展示了它在没有endl的情况下工作。 - Shafik Yaghmour
啊,是的,你说得对。(我在测试代码的其他地方有一个std::cout << std::endl)。看起来std::variant<>好像可以从任何东西隐式构造?也许这是std::variant实现中的一个bug? - alfC

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接