Esprimi una delle molteplici opzioni in un modo carino

Esprimi una delle molteplici opzioni in un modo carino

Spesso ci troviamo a scrivere istruzioni if ​​in cui una variabile viene confrontata con più valori per verificare se corrisponde a uno di essi o che non corrisponde a nessuno. Ecco un esempio:

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";
}

Questo esempio ha tre valori di confronto per ogni caso, ma potrebbe essere 5, 10 o qualsiasi numero. Se sono troppi, forse dovrebbe essere adottato un approccio diverso. Tuttavia, la domanda è:come lo esprimiamo in un modo più semplice in C++, piuttosto che in una condizione long if?

Prima di continuare, ignora i numeri magici usati qui. Sono solo alcuni valori per mostrare il punto in modo semplice. Nel codice reale, dovresti usare enums, valori constexpr o macro (opzione malvagia). Ecco un esempio reale da alcuni codici con cui sto lavorando:

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

In SQL, possiamo scrivere istruzioni nelle seguenti forme:

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

Possiamo ottenere qualcosa di simile a SQL IN operatore in C++? Bene, in C++11 abbiamo std::all_of , std::any_of , std::none_of che può aiutare a esprimere lo stesso intento. Usando any_of e none_of possiamo riscrivere le due affermazioni if ​​di cui sopra come segue:

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";
}

Questo è molto dettagliato e, personalmente, non mi piace affatto. Non mi piace perché devi mettere i valori in un contenitore (un vector qui) e devi fornire iteratori all'inizio e alla fine del contenitore, oltre a un lambda per confrontare il tuo valore con ogni valore nel contenitore. Ci sono modi più semplici per farlo.

In C++20 abbiamo gli intervalli e la libreria degli intervalli fornisce una nuova implementazione per questi algoritmi, chiamata std::ranges::all_of , std::ranges::any_of , std::ranges::none_of . Usando questi algoritmi, possiamo semplicemente fornire un intervallo (come un contenitore) invece della coppia di iteratori inizio-fine. Pertanto, il nostro codice sarebbe simile al seguente:

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";
}

Questo è più semplice, ma non sono ancora soddisfatto. Dobbiamo ancora mettere i valori in un contenitore. Non è possibile semplificare il codice come segue:

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

È più vicino al codice che vorrei essere in grado di scrivere. Quel codice è mostrato di seguito:

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";
}

Questa è una sequenza di valori e una variabile sul posto, invece di una lambda. Questo non funziona immediatamente con nessun algoritmo standard, ma possiamo facilmente scrivere questo any_of e none_of noi stessi. Una possibile implementazione è elencata nel seguente snippet:

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

So che la gente dirà che std::initializer_list non è un contenitore e non dovrebbe essere usato come tale, ma non credo che sia usato come tale in questo frammento. Fondamentalmente contiene una sequenza temporanea di valori che vengono ripetuti con std::any_of e std::none_of_ algoritmi. Questa implementazione ci consentirà di scrivere codice come nello snippet precedente.

Tuttavia, c'è un'altra semplificazione a cui potremmo pensare, che è mostrata qui:

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";
}

Non esiste una sequenza di valori, solo un numero variabile di argomenti, passati a una funzione. Ciò significa, questa volta, l'implementazione di any_of e none_of dovrebbe essere basato su modelli variadici. Il modo in cui l'ho scritto, usando le espressioni fold, è il seguente:

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

Questa volta, la variabile viene fornita come primo argomento e i valori su cui eseguire il test la seguono. Sfortunatamente, questa implementazione consente chiamate senza alcun valore con cui confrontare, come any_of(option) . Tuttavia, ciò è relativamente semplice da evitare aggiungendo un static_assert dichiarazione, come segue:

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

Se non ti piace static_assert se stai usando C++20, puoi usare i vincoli per richiedere che il pacchetto di parametri abbia almeno un elemento. La modifica è relativamente semplice e si presenta come segue:

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

Come puoi vedere, C++ offre diversi modi standard e fai-da-te per sostituire alcune categorie di istruzioni if ​​con una chiamata di funzione.