exterior - ¿Cómo hacer un visitante variante de C++ más seguro, similar a las declaraciones de cambio?
declaracion de cambio concepto (2)
El patrón que mucha gente usa con las variantes de C ++ 17 / boost parece muy similar a las declaraciones de cambio. Por ejemplo: ( fragmento 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);
El problema es cuando coloca el tipo incorrecto en el visitante o cambia la firma de la variante, pero se olvida de cambiar el visitante. En lugar de obtener un error de compilación, se le llamará al lambda incorrecto, generalmente el predeterminado, o podría obtener una conversión implícita que no planeó. Por ejemplo:
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);
Las declaraciones de cambio en las clases de enumeración son mucho más seguras, porque no se puede escribir una declaración de caso con un valor que no sea parte de la enumeración. De manera similar, creo que sería muy útil si un visitante variante se limitara a un subconjunto de los tipos contenidos en la variante, más un controlador predeterminado. ¿Es posible implementar algo así?
EDITAR: s / conversión implícita / conversión implícita /
EDIT2: Me gustaría tener un manejador significativo para capturar todos [](auto)
. Sé que eliminarlo causará errores de compilación si no maneja cada tipo en la variante, pero eso también elimina la funcionalidad del patrón de visitantes.
Puede agregar una capa adicional para agregar esos controles adicionales, por ejemplo, algo como:
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);
}
Con uso:
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);
Si solo desea permitir un subconjunto de tipos, puede usar static_assert
al comienzo de la lambda, por ejemplo:
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);
Esto fallará si agrega o cambia un tipo en la variante, o agrega uno, porque T
debe ser exactamente uno de los tipos dados.
También puede jugar con su variante de std::visit
, por ejemplo, con un visitante "predeterminado" como:
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 agrega un tipo que no sea int
, long
, double
o std::string
, entonces el operador visit_only_for
call coincidirá y tendrá una llamada ambigua (entre este y el predeterminado).
Esto también debería funcionar sin valor predeterminado porque el operador de llamada visit_only_for
será coincidente, pero como se elimina, obtendrá un error en tiempo de compilación.