37 votes

Comment créer un visiteur de variante C++ plus sûr, similaire aux instructions switch ?

Le modèle que beaucoup de gens utilisent avec les variantes C++17 / boost ressemble beaucoup aux instructions switch. Par exemple : ( extrait de cppreference.com )

std::variant<int, long, double, std::string> v = ...;

std::visit(overloaded {
    [](auto arg) { std::cout << arg << ' '; },
    [](double arg) { std::cout << std::fixed << arg << ' '; },
    [](const std::string& arg) { std::cout << std::quoted(arg) << ' '; },
}, v);

Le problème se pose lorsque l'on met le mauvais type dans le visiteur ou que l'on modifie la signature de la variante, mais que l'on oublie de modifier le visiteur. Au lieu d'obtenir une erreur de compilation, vous aurez le mauvais lambda appelé, généralement celui par défaut, ou vous pourriez obtenir une conversion implicite que vous n'avez pas prévue. Par exemple, vous pouvez obtenir une conversion implicite que vous n'avez pas prévue :

v = 2.2;
std::visit(overloaded {
    [](auto arg) { std::cout << arg << ' '; },
    [](float arg) { std::cout << std::fixed << arg << ' '; } // oops, this won't be called
}, v);

Les instructions de commutation sur les classes enum sont beaucoup plus sûres, car vous ne pouvez pas écrire une instruction case en utilisant une valeur qui ne fait pas partie de l'enum. De même, je pense qu'il serait très utile qu'un visiteur de variante soit limité à un sous-ensemble des types contenus dans la variante, plus un gestionnaire par défaut. Est-il possible d'implémenter quelque chose comme cela ?

EDIT : s/implicit cast/implicit conversion/

EDIT2 : J'aimerais avoir un "fourre-tout" significatif. [](auto) gestionnaire. Je sais que sa suppression provoquera des erreurs de compilation si vous ne gérez pas tous les types de la variante, mais cela supprime également une fonctionnalité du modèle de visiteur.

0 votes

L'extrait juste au-dessus de celui-ci sur en.cppreference ne fait-il pas exactement ce que vous voulez ?

3 votes

Hier, j'ai appris qu'il n'existe pas de "cast implicite", car tous les cast sont explicites. La phrase que vous recherchez est "conversion implicite" :) stackoverflow.com/a/45672844/3560202

0 votes

@Holt, celui avec constexpr if ? Je pense qu'elle présente le même écueil.

31voto

Holt Points 6689

Si vous souhaitez n'autoriser qu'un sous-ensemble de types, vous pouvez utiliser un static_assert au début du lambda, par exemple :

template <typename T, typename... Args>
struct is_one_of: 
    std::disjunction<std::is_same<std::decay_t<T>, Args>...> {};

std::visit([](auto&& arg) {
    static_assert(is_one_of<decltype(arg), 
                            int, long, double, std::string>{}, "Non matching type.");
    using T = std::decay_t<decltype(arg)>;
    if constexpr (std::is_same_v<T, int>)
        std::cout << "int with value " << arg << '\n';
    else if constexpr (std::is_same_v<T, double>)
        std::cout << "double with value " << arg << '\n';
    else 
        std::cout << "default with value " << arg << '\n';
}, v);

Cela échoue si vous ajoutez ou modifiez un type dans la variante, ou si vous en ajoutez un, car T doit être exactement un des types donnés.

Vous pouvez également jouer avec votre variante de std::visit par exemple avec un visiteur "par défaut" comme :

template <typename... Args>
struct visit_only_for {
    // delete templated call operator
    template <typename T>
    std::enable_if_t<!is_one_of<T, Args...>{}> operator()(T&&) const = delete;
};

// then
std::visit(overloaded {
    visit_only_for<int, long, double, std::string>{}, // here
    [](auto arg) { std::cout << arg << ' '; },
    [](double arg) { std::cout << std::fixed << arg << ' '; },
    [](const std::string& arg) { std::cout << std::quoted(arg) << ' '; },
}, v);

Si vous ajoutez un type qui n'est pas l'un des types suivants int , long , double o std::string , alors le visit_only_for L'opérateur de l'appel sera en correspondance et vous aurez un appel ambigu (entre celui-ci et l'appel par défaut).

Cela devrait également fonctionner sans défaut parce que le visit_only_for sera égal, mais comme il est supprimé, vous obtiendrez une erreur de compilation.

3 votes

visit_only_for peut voir son opérateur supprimé. (cela éviterait d'introduire des ret_t ).

0 votes

@Jarod42 c'est vrai, je n'ai pas l'habitude de supprimer ce genre de fonctions, mais c'est plus joli ;) Réponse mise à jour.

0 votes

template <typename... Args> visit_only_for<Args...> make_visit_only_for(const std::variant<Args...>&) { return {}; } pourrait également être un bon complément.

1voto

Jarod42 Points 15729

Vous pouvez ajouter une couche supplémentaire pour ajouter ce contrôle supplémentaire, par exemple quelque chose comme :

template <typename Ret, typename ... Ts> struct IVisitorHelper;

template <typename Ret> struct IVisitorHelper<Ret> {};

template <typename Ret, typename T>
struct IVisitorHelper<Ret, T>
{
    virtual ~IVisitorHelper() = default;
    virtual Ret operator()(T) const = 0;
};

template <typename Ret, typename T, typename T2, typename ... Ts>
struct IVisitorHelper<Ret, T, T2, Ts...> : IVisitorHelper<Ret, T2, Ts...>
{
    using IVisitorHelper<Ret, T2, Ts...>::operator();
    virtual Ret operator()(T) const = 0;
};

template <typename Ret, typename V> struct IVarianVisitor;

template <typename Ret, typename ... Ts>
struct IVarianVisitor<Ret, std::variant<Ts...>> : IVisitorHelper<Ret, Ts...>
{
};

template <typename Ret, typename V>
Ret my_visit(const IVarianVisitor<Ret, std::decay_t<V>>& v, V&& var)
{
    return std::visit(v, var);
}

Avec usage :

struct Visitor : IVarianVisitor<void, std::variant<double, std::string>>
{
    void operator() (double) const override { std::cout << "double\n"; }
    void operator() (std::string) const override { std::cout << "string\n"; }
};

std::variant<double, std::string> v = //...;
my_visit(Visitor{}, v);

0voto

Martin Ba Points 10243

Un peu sur la base de la visit_only_for exemple par Holt Je suis en train d'essayer quelque chose de ce genre pour avoir une "étiquette" intégrée à mon site web. std::visit ce qui évite d'oublier des gestionnaires/opérateurs explicites :

//! struct visit_all_types_explicitly
//!
//! If this callable is used in the overload set for std::visit
//! its templated call operator will be bound to any type
//! that is not explicitly handled by a better match.
//! Since the instantiation of operator()<T> will trigger
//! a static_assert below, using this in std::visit forces
//! the user to handle all type cases.
//! Specifically, since the templated call operator is a
//! better match than call operators found via implicit argument
//! conversion, one is forced to implement all types even if
//! they are implicitly convertible without warning.
struct visit_all_types_explicitly {
    template<class> static inline constexpr bool always_false_v = false;

    // Note: Uses (T const&) instead of (T&&) because the const-ref version
    //       is a better "match" than the universal-ref version, thereby
    //       preventing the use of this in a context where another
    //       templated call operator is supplied.
    template<typename T>
    void operator()(T const& arg) const {
        static_assert(always_false_v<T>, "There are unbound type cases! [visit_all_types_explicitly]");
    }
};

using MyVariant = std::variant<int, double>;

void test_visit() {
    const MyVariant val1 = 42;

    // This compiles:
    std::visit(
        overloaded{
            kse::visit_all_types_explicitly(),
            [](double arg) {},
            [](int arg) {},
        },
        val1
        );

    // does not compile because missing int-operator causes
    // visit_all_types_explicitly::operator()<int> to be instantiated
    std::visit(
        overloaded{
            visit_all_types_explicitly(),
            [](double arg) {},
            // [](int arg) {  },
        },
        val1
        );

    // does also not compile: (with static assert from visit_all_types_explicitly)
    std::visit(
        overloaded{
            visit_all_types_explicitly(),
            [](double arg) {},
            // [](int arg) {  },
            [](auto&& arg) {}
        },
        val1
    );

    // does also not compile: (with std::visit not being able to match the overloads)
    std::visit(
        overloaded{
            visit_all_types_explicitly(),
            [](double arg) {},
            // [](int arg) {  },
            [](auto const& arg) {}
        },
        val1
    );
}

Pour l'instant, cette semble pour faire ce que je veux, et ce que l'OP a demandé :

Au lieu d'obtenir une erreur de compilation, vous aurez le mauvais lambda appelé, généralement celui par défaut, ou vous obtiendrez une conversion implicite que vous n'avez pas prévue.

Vous ne pouvez intentionnellement pas combiner cela avec un gestionnaire "par défaut" / automatique.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X