Overdose di Lambda

Overdose di Lambda

I Lambda sono una bella aggiunta recente a C++. Sono fantastici, alla moda e tendono a essere abusati e usati in modo improprio.

Da quando le espressioni lambda sono emerse in C++ 11 e hanno ottenuto un enorme aumento dell'usabilità in C++ 14, sono state di gran moda. Non fraintendermi. Le Lambda sono davvero utile e interessante e tutto. Ma ultimamente la lettura dei post del blog, del canale CppLang Slack e di altre fonti mi ha dato l'impressione che alcune persone utilizzino i lambda in modi in cui non dovrebbero essere utilizzati.

I Lambda non sostituiscono le normali funzioni

Diamo un'occhiata a questo esempio:

int main() {
  auto sequence = [](size_t i){
    std::vector<size_t> result(i);
    std::iota(begin(result), end(result), 0);
    return result;
  };

  auto print = [](auto const& container) {
    for (auto&& e : container) {
      std::cout << e << ' ';
    }
    std::cout << '\n';
  };

  print(sequence(22));
}

Qui, il main function contiene la definizione di due lambda che agiscono come normali funzioni. Il lavoro effettivo svolto in main è solo l'ultima riga, ma il corpo della funzione viene gonfiato fino a 14 righe in totale. Se un lettore vuole sapere cosa main devono scorrere oltre le lambda, il che richiede tempo inutile. Può essere peggio, ad es. se il codice di interesse è intercalato con definizioni lambda:

int main() {
  auto sequence = [](size_t i){
    std::vector<size_t> result(i);
    std::iota(begin(result), end(result), 0);
    return result;
  };

  auto s = sequence(22);
  s.push_back(42);

  auto print = [](auto const& container) {
    for (auto&& e : container) {
      std::cout << e << ' ';
    }
    std::cout << '\n';
  };

  print(s);
}

Ora c'è un ulteriore sforzo per il lettore nel determinare quali righe sono importanti da leggere e quali no. Diamo un'occhiata a un'implementazione più naturale:

auto sequence(size_t i) {
    std::vector<size_t> result(i);
    std::iota(begin(result), end(result), 0);
    return result;
}

template<class C>
auto print(C const& container) {
    for (auto&& e : container) {
      std::cout << e << ' ';
    }
    std::cout << '\n';
}

int main() {
  auto s = sequence(22);
  s.push_back(42);
  print(s);
}

Questo è più o meno lo stesso codice, a parte il piccolo boilerplate necessario per dichiarare print e template . Tuttavia, la leggibilità è notevolmente migliorata:main sono solo tre righe che possono essere sufficienti per sapere cosa sta succedendo. Se hai bisogno di sapere cosa sequence fa esattamente, perché ha un nome scadente, quindi puoi esaminare la funzione come al solito.

Potrebbero esserci due piccoli svantaggi nell'avere funzioni reali invece di lambda:i nomi delle funzioni saranno visibili al linker al di fuori dell'unità di traduzione, ovvero hanno un collegamento esterno, che può anche influenzare l'inlining. In secondo luogo, le regole di ricerca possono differire, il che potrebbe effettivamente essere fonte di preoccupazione per qualcosa chiamato print . Entrambi i problemi possono, tuttavia, essere risolti facilmente utilizzando spazi dei nomi anonimi per il collegamento interno e uno spazio dei nomi denominato per la ricerca, se assolutamente necessario.

Lambda extra lunghe

Una variante del problema precedente consiste nel rendere le lambda necessarie più lunghe di poche righe. Anche se devi usare una lambda, ad es. poiché hai acquisizioni e/o hai effettivamente bisogno dell'oggetto funzione che crea, i lambda dovrebbero essere brevi, anche più brevi della tua funzione media.

Il motivo è che le lambda di solito sono solo un elemento di un contesto più ampio, ad es. una chiamata di algoritmo. Se un singolo elemento è più grande dell'intero resto del suo contesto, il lettore si concentrerà sul singolo elemento anziché su quel contesto. Inoltre, è probabile che i corpi lambda più grandi abbiano un livello di astrazione inferiore rispetto alla funzione circostante, quindi la funzione nel suo insieme viola il principio SLA.

Non c'è nulla che vieti l'estrazione di funzioni da un corpo lambda come si farebbe da una normale funzione per mantenerla breve e leggibile.

Il clamore funzionale

Ci sono alcune persone là fuori che si divertono nella dolce purezza della programmazione funzionale. Al giorno d'oggi è molto raro vedere una conferenza senza almeno alcuni discorsi sul C++ funzionale. Le persone iniziano a confrontare C++ con Haskell e producono lambda che restituiscono lambda che generano altri lambda che... hai capito.

I principi funzionali sono un argomento molto interessante (io stesso partecipo o guardo quei discorsi ogni volta che ne incontro uno), ed è davvero fantastico avere tali strutture nella lingua. Alcuni di essi sono persino necessari per scrivere software parallelo scalabile. Ma il C++ non è e non sarà mai un linguaggio funzionale, come se non fosse nemmeno un linguaggio orientato agli oggetti.

Invece, C++ è un linguaggio multiparadigma. È una scatola piena di strumenti diversi, e noi facciamo del nostro meglio per usarli solo dove sono appropriati e nel modo che ne faccia il miglior uso. Non ha senso usare un unico strumento (es. lambdas) ovunque possiamo, in ogni modo possibile, solo perché finalmente lo abbiamo a nostra disposizione.