web-dev-qa-db-fra.com

Impossible de diffuser std :: endl avec l'opérateur surchargé << () pour std :: variant

Cette réponse décrit comment diffuser un std::variant Autonome. Cependant, cela ne semble pas fonctionner lorsque std::variant Est stocké dans un std::unordered_map.

Ce qui suit exemple :

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

ne parvient pas à compiler avec:

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

Pourquoi cela arrive-t-il? Comment est-il possible de le réparer?

23
Dev Null

Dans [temp.arg.explicit]/ , nous avons cette phrase étonnante:

Un pack de paramètres de modèle de fin non déduit autrement sera déduit d'une séquence vide d'arguments de modèle.

Qu'est-ce que ça veut dire? Qu'est-ce qu'un pack de paramètres de modèle de fin? Qu'est-ce qui n'est pas déduit autrement? Ce sont toutes de bonnes questions qui n'ont pas vraiment de réponses. Mais cela a des conséquences très intéressantes. Considérer:

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

C'est ... bien formé. Nous ne pouvons pas déduire Ts... donc nous le déduisons comme vide. Cela nous laisse avec std::Tuple<>, qui est un type parfaitement valide - et un type parfaitement valide qui peut même être instancié avec {}. Donc ça compile!

Alors, que se passe-t-il lorsque la chose que nous déduisons du pack de paramètres vide que nous avons évoqué n'est pas un type valide? Voici n exemple :

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

Le operator<< est un candidat potentiel, mais la déduction échoue ... du moins semble-t-il. Jusqu'à ce que nous évoquions Ts... comme vide. Mais Y<> est un type non valide! Nous n'essayons même pas de découvrir que nous ne pouvons pas construire un Y<> de std::endl - nous avons déjà échoué .

C'est fondamentalement la même situation que vous avez avec variant, car variant<> n'est pas un type valide.

La solution simple consiste à simplement changer votre modèle de fonction en prenant variant<Ts...> à un variant<T, Ts...>. Cela ne peut plus déduire à variant<>, ce qui n'est même pas possible, donc nous n'avons pas de problème.

32
Barry

Pour une raison quelconque, votre code (qui me semble correct) tente d'instancier std::variant<> (alternatives vides) à la fois dans clang et gcc.

La solution de contournement que j'ai trouvée consiste à créer un modèle pour une variante spécifiquement non vide. Puisque std::variant ne peut pas être vide de toute façon, je pense qu'il est généralement bon d'écrire des fonctions génériques pour des variantes non vides.

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

Avec ce changement, votre code fonctionne pour moi.


J'ai aussi compris que si std::variant avait une spécialisation de std::variant<> sans un constructeur à argument unique, ce problème ne se serait pas produit en premier lieu. Voir les premières lignes dans https://godbolt.org/z/VGih_4 et comment cela fonctionne.

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

Je fais cela juste pour illustrer le point, je ne recommande pas nécessairement de le faire.

7
alfC

Le problème est le std::endl mais je ne comprends pas pourquoi votre surcharge est meilleure que celle de std :: basic_ostream :: operator << , voir exemple en direct godbolt :

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

et en supprimant le std::endl résout en effet le problème, voyez-le en direct sur Wandbox .

Comme le souligne alfC, la modification de votre opérateur pour interdire une variante vide résout effectivement le problème, voir en direct :

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