Expresar una de múltiples opciones de una manera agradable

Expresar una de múltiples opciones de una manera agradable

A menudo nos encontramos escribiendo sentencias if en las que una variable se compara con varios valores, ya sea para comprobar si coincide con uno de ellos o si no coincide con ninguno. Aquí hay un ejemplo:

int option = ...;

// at least a value is matched
if (option == 12 || option == 23 || option == 42)
{
   std::cout << "it's a good option\n";
}

// no value is matched
if (option != 12 && option != 23 && option != 42)
{
   std::cout << "it's a bad option\n";
}

Este ejemplo tiene tres valores de comparación para cada caso, pero podría ser 5, 10 o cualquier número. Si son demasiados, quizás se deba adoptar un enfoque diferente. Sin embargo, la pregunta es, ¿cómo expresamos esto de una manera más simple en C++, en lugar de una condición if larga?

Antes de continuar, ignore los números mágicos que se usan aquí. Son solo algunos valores para mostrar el punto de una manera simple. En el código real, debe usar enumeraciones, valores constexpr o macros (opción malvada). Aquí hay un ejemplo real de un código con el que estoy trabajando:

if (iCtrlType != CTRLTYPE_BUTTON &&
    iCtrlType != CTRLTYPE_CHECKBOX &&
    iCtrlType != CTRLTYPE_COMBOBOX &&
    iCtrlType != CTRLTYPE_COMBOGRID &&
    iCtrlType != CTRLTYPE_DROPDOWNCAL &&
    iCtrlType != CTRLTYPE_EDIT)
{
   // do something
}

En SQL, podemos escribir sentencias de las siguientes formas:

SELECT column-names
FROM table-name
WHERE column-name IN (values) 
SELECT column-names
FROM table-name
WHERE column-name NOT IN (values)

¿Podemos lograr algo similar al SQL IN? operador en C++? Bueno, en C++11 tenemos std::all_of , std::any_of , std::none_of que pueden ayudar a expresar la misma intención. Usando any_of y none_of podemos reescribir las dos declaraciones if anteriores de la siguiente manera:

std::vector<int> good_options{ 12, 23, 42 };
if (std::any_of(good_options.begin(), good_options.end(), [option](int const o) { return option == o; }))
{
   std::cout << "it's a good option\n";
}

if (std::none_of(good_options.begin(), good_options.end(), [option](int const o) { return option == o; }))
{
   std::cout << "it's a bad option\n";
}

Esto es muy detallado, y personalmente, no me gusta nada. No me gusta porque necesitas poner los valores en un contenedor (un vector aquí) y debe proporcionar iteradores al principio y al final del contenedor, así como una lambda para comparar su valor con cada valor en el contenedor. Hay formas más sencillas de hacerlo.

En C++20, tenemos rangos, y la biblioteca de rangos proporciona una nueva implementación para estos algoritmos, llamada std::ranges::all_of , std::ranges::any_of , std::ranges::none_of . Con estos algoritmos, podemos simplemente proporcionar un rango (como un contenedor) en lugar del par de iteradores de inicio y fin. Por lo tanto, nuestro código se vería así:

std::vector<int> good_options{ 12, 23, 42 };

if (std::ranges::any_of(good_options, [option](int const o) { return option == o; }))
{
   std::cout << "it's a good option\n";
}

if (std::ranges::none_of(good_options, [option](int const o) { return option == o; }))
{
   std::cout << "it's a bad option\n";
}

Esto es más simple, pero todavía no estoy satisfecho. Todavía tenemos que poner los valores en un contenedor. No es posible simplificar el código de la siguiente manera:

if (std::ranges::any_of({ 12, 23, 42 }, [option](int const o) { return option == o; }))
{
   std::cout << "it's a good option\n";
}

Eso está más cerca del código que me gustaría poder escribir. Ese código se muestra a continuación:

if (any_of({ 12, 23, 42 }, option))
{
   std::cout << "it's a good option\n";
}

if (none_of({ 12, 23, 42 }, option))
{
    std::cout << "it's a bad option\n";
}

Esta es una secuencia de valores y una variable en el lugar, en lugar de una lambda. Esto no funciona de inmediato con ningún algoritmo estándar, pero podemos escribir fácilmente este any_of y none_of Nosotros mismos. Una posible implementación se enumera en el siguiente fragmento:

template <typename T>
bool any_of(std::initializer_list<T> r, T const v)
{
   return std::any_of(r.begin(), r.end(), [v](int const x) { return v == x; });
}

template <typename T>
bool none_of(std::initializer_list<T> r, T const v)
{
   return std::none_of(r.begin(), r.end(), [v](int const x) { return v == x; });
}

Sé que la gente dirá que std::initializer_list no es un contenedor y no debe usarse como tal, pero realmente no creo que se use como tal en este fragmento. Básicamente contiene una secuencia temporal de valores que se repiten con el std::any_of y std::none_of_ algoritmos Esta implementación nos permitirá escribir código como en el fragmento anterior.

Sin embargo, hay una simplificación más en la que podríamos pensar, que se muestra aquí:

if (any_of(option, 12, 23, 42))
{
   std::cout << "it's a good option\n";
}

if (none_of(option, 12, 23, 42))
{
   std::cout << "it's a bad option\n";
}

No hay una secuencia de valores, solo un número variable de argumentos que se pasan a una función. Eso significa, esta vez, la implementación de any_of y none_of debe basarse en plantillas variadas. La forma en que lo escribí, usando expresiones de pliegue, es la siguiente:

template <typename T, typename ...Args>
bool any_of(T const v, Args&&... args)
{
   return ((args == v) || ...);
}

template <typename T, typename ...Args>
bool none_of(T const v, Args&&... args)
{
   return ((args != v) && ...);
}

Esta vez, la variable se proporciona como primer argumento, y los valores contra los que se va a realizar la prueba la siguen. Desafortunadamente, esta implementación permite llamadas sin valores con los que comparar, como any_of(option) . Sin embargo, eso es relativamente simple de evitar agregando un static_assert declaración, de la siguiente manera:

template <typename T, typename ...Args>
bool any_of(T const v, Args&&... args)
{
   static_assert(sizeof...(args) > 0, "You need to supply at least one argument.");
   return ((args == v) || ...);
}

template <typename T, typename ...Args>
bool none_of(T const v, Args&&... args)
{
   static_assert(sizeof...(args) > 0, "You need to supply at least one argument.");
   return ((args != v) && ...);
}

Si no te gusta static_assert s y está usando C++20, entonces puede usar restricciones para requerir que el paquete de parámetros tenga al menos un elemento. El cambio es relativamente simple y tiene el siguiente aspecto:

template <typename T, typename ...Args>
bool any_of(T const v, Args&&... args) requires (sizeof...(args) > 0)
{
   return ((args == v) || ...);
}

template <typename T, typename ...Args>
bool none_of(T const v, Args&&... args) requires (sizeof...(args) > 0)
{
   return ((args != v) && ...);
}

Como puede ver, C++ ofrece diferentes formas estándar y de bricolaje de reemplazar alguna categoría de declaraciones if con una llamada de función.