web-dev-qa-db-fra.com

Comment rendre 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 est lorsque vous mettez le mauvais type dans le visiteur ou changez la signature de variante, mais oubliez de changer 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 planifiée. Par exemple:

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 switch sur les classes enum sont beaucoup plus sécurisées, car vous ne pouvez pas écrire une instruction case en utilisant une valeur qui ne fait pas partie de l'énumération. De même, je pense qu'il serait très utile qu'un visiteur variant soit limité à un sous-ensemble des types contenus dans la variante, plus un gestionnaire par défaut. Est-il possible de mettre en œuvre quelque chose comme ça?

EDIT: s/cast implicite/conversion implicite /

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

30
Michał Brzozowski

Si vous souhaitez autoriser uniquement un sous-ensemble de types, vous pouvez utiliser un static_assert au début de la 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 échouera si vous ajoutez ou modifiez un type dans la variante, ou en ajoutez un, car T doit être exactement l'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 différent de int, long, double ou std::string, puis le visit_only_for L'opérateur d'appel correspondra et vous aurez un appel ambigu (entre celui-ci et celui par défaut).

Cela devrait également fonctionner sans défaut car le visit_only_for l'opérateur d'appel correspondra, mais comme il est supprimé, vous obtiendrez une erreur au moment de la compilation.

24
Holt

Vous pouvez ajouter une couche supplémentaire pour ajouter ces contrôles supplémentaires, 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 utilisation:

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);
1
Jarod42