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.
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.
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);