Parametri variadici non terminali e valori predefiniti

Parametri variadici non terminali e valori predefiniti

Attualmente, a partire da C++ 20, non è disponibile il supporto per i cosiddetti argomenti variadici non terminali. Ad esempio, non possiamo scrivere:

template <class ...Args> void func(Args&& ...args, int num=42);
func(10, 20); // error

Come puoi vedere, volevo 10 e 20 da passare come ...args e 42 come valore predefinito per num . I compilatori attualmente non possono risolvere questo codice.

In questo post del blog, vorrei mostrarti un paio di trucchi che puoi implementare per evitare questo problema. Conoscere queste tecniche potrebbe aiutare con cose come le funzioni di registrazione in cui potremmo avere std::source_location alla fine di una dichiarazione di funzione.

Il std::source_location Caso

L'ultima volta ti ho mostrato un paio di tecniche e miglioramenti per le funzioni di registrazione. Abbiamo discusso di __FILE__ , __LINE__ macro, come racchiuderli in funzioni che possono richiedere un numero variabile di argomenti. E in seguito ho introdotto anche std::source_location da C++ 20. Un problema che potremmo avere è che il codice seguente non viene compilato:

template <typename ...Args>
void log(Args&& ...args, source_location& loc = source_location::current()) { }

log("hello world", 42);

Come il codice dell'introduzione, voglio passare un numero variabile di argomenti, ma allo stesso tempo "correggere" l'ultimo e fornire un valore predefinito.

Ecco le opzioni da considerare:

  • Fornire la funzione sovraccarica uno, due, tre parametri (come prima C++11).
  • Utilizza un modello di funzione, ma specifica i parametri del modello:come log<int, double>(42, 100.75); .
  • Utilizza una guida alla detrazione personalizzata.
  • Usa una piccola struttura e passa source_location come parametro a un costruttore. Qualcosa come Logger().log(...) .
  • Usa tuple e poi la chiamata sarebbe la seguente:log(std::make_tuple("hello", 42, 100.076)); .
  • Aspettare il nuovo standard C++ dove questo problema è stato risolto?
  • Un approccio diverso con << ?

Esaminiamo ora quell'elenco.

1. Sovraccarichi delle funzioni

Probabilmente è l'approccio più diretto. Perché non scrivere due o tre sovraccarichi di funzioni e consentire il passaggio di 1, 2 o 3 parametri? Questa era una tecnica popolare prima di C++11, in cui gli argomenti variadici non erano possibili.

template <typename T>
void log(T&& arg, source_location& loc = current());
template <typename T, typename U>
void log(T&& t, U&& u, source_location& loc = current());
template <typename T, typename U, typename V>
void log(T&& t, U&& u, V&& v, source_location& loc = current());

Anche se questo codice potrebbe non essere il migliore per una funzione di libreria generica, a volte potrebbe essere la soluzione più semplice per piccoli progetti.

Ok, ma proviamo qualcosa di più complicato.

2. Fornisci tipi di argomenti espliciti

Il problema principale con gli argomenti variadici non terminali è che il compilatore non può risolvere e abbinare adeguatamente gli argomenti.

Allora perché non aiutarlo?

Quello che possiamo fare è scrivere quali tipi vorremmo gestire e quindi dovrebbe funzionare:

#include <iostream>
#include <source_location>
#include <string>

template <typename... Ts>
void log(Ts&&... ts, const std::source_location& loc = std::source_location::current()) {
    std::cout << loc.function_name() << " line " << loc.line() << ": ";
        ((std::cout << std::forward<Ts>(ts) << " "), ...);
        std::cout << '\n';
}

int main() {
    log<int, int, std::string>(42, 100, "hello world");
    log<double, std::string>(10.75, "an important parameter");
}

Gioca a @Compiler Explorer

Come puoi vedere, ho specificato tutti i tipi e, in questo modo, il compilatore può creare correttamente la specializzazione del modello finale.

E questo ci indirizza in una direzione...

3. Guide alle detrazioni

Come puoi vedere dal punto precedente, se forniamo argomenti corretti, il compilatore può risolverlo.

In C++17, abbiamo un altro strumento che può aiutarci:le guide alla deduzione e la deduzione dell'argomento del modello di classe (CTAD).

Quello che possiamo fare è quanto segue:

template <typename... Ts>
struct log {    
    log(Ts&&... ts, std::source_location& loc = std::source_location::current()) {
        std::cout << loc.function_name() << " line " << loc.line() << ": ";
        ((std::cout << std::forward<Ts>(ts) << " "), ...);
        std::cout << '\n';
    }
};

template <typename... Ts>
log(Ts&&...) -> log<Ts...>;

La guida alla deduzione in basso dice al compilatore di compilare log<Ts...> quando vede log(Ts...) . Il vantaggio principale qui è che la guida alla deduzione è uno strato tra il nostro costruttore variadico effettivo con l'argomento predefinito. In questo modo, il compilatore ha un lavoro più semplice.

E gioca con l'esempio completo di seguito:

#include <iostream>
#include <source_location>
#include <string>

template <typename... Ts>
struct log
{    
    log(Ts&&... ts, const std::source_location& loc = std::source_location::current()) {
        std::cout << loc.function_name() << " line " << loc.line() << ": ";
        ((std::cout << std::forward<Ts>(ts) << " "), ...);
        std::cout << '\n';
    }
};

template <typename... Ts>
log(Ts&&...) -> log<Ts...>;

int main() {
    log(42, 100, "hello world");
    log(10.75, "an important parameter");
}

Gioca a @Compiler Explorer

Questo esempio ci ha anche mostrato come passare da una funzione a un costruttore struct e leva separato. Potrebbe esserci un problema quando devi restituire qualcosa da una tale funzione di registrazione, però.

Quello che possiamo fare è adottare questo approccio ed espanderci. Vedi sotto.

4. Utilizzo di un costruttore

Che ne dici di usare solo il costruttore per prendere la posizione di origine e quindi esporre un log separato funzione?

Dai un'occhiata:

#include <iostream>
#include <string_view>
#include <source_location>
#include <fmt/core.h>

struct Logger {
    Logger(std::source_location l = std::source_location::current()) : loc(std::move(l)) { }
    
    template <typename ...Args>
    void debug(std::string_view format, Args&& ...args) {
	    std::cout << fmt::format("{}({}) ", loc.file_name(), loc.line())
                  << fmt::format(format, std::forward<Args>(args)...) << '\n';
    }
    
private:
    std::source_location loc;    
};
 
int main() {
    std::cout << sizeof(std::source_location) << '\n';
    Logger().debug("{}, {}", "hello", "world");
    Logger().debug("{}, {}", 10, 42);
}

Gioca a @Compiler Explorer

Come puoi vedere, ho usato un costruttore per l'argomento predefinito e poi c'è un'altra funzione regolare che si occupa dell'elenco variadico. Con una normale funzione membro, puoi anche restituire valori, se necessario.

5. Usa una tupla

Per completezza devo citare anche una tecnica. Quello che possiamo fare è racchiudere tutti gli argomenti variadici in std::tuple :

#include <iostream>
#include <source_location>
#include <string>
#include <tuple>

template <typename... Ts>
void log(std::tuple<Ts...> tup, const std::source_location& loc = std::source_location::current()) {
    std::cout << loc.function_name() << " line " << loc.line() << ": ";
    std::apply([](auto&&... args) {
        ((std::cout << args << ' '), ...);
    }, tup);
    std::cout << '\n';
}

int main() {
    log(std::make_tuple(42, 100, "hello world"));
    log(std::make_tuple(10.75, "an important parameter"));
}

Come puoi vedere, dobbiamo usare std::apply , che "traduce" la tupla in un elenco di argomenti.

6. Un oggetto flusso

Finora, abbiamo discusso delle funzioni regolari o di un'opzione per "convertirlo" in una struttura/classe separata. Ma c'è un altro approccio.

In un articolo sul blog di Arthur O'Dwyer - Come sostituire __FILE__ con source_location in una macro di registrazione. Propone di utilizzare un oggetto stream e quindi di passare argomenti tramite << operatori.

NewDebugStream nds;
nds << "Hello world! " << 42 << "\n";

7. Aspetta C++ 23 o successivo?

Come puoi immaginare, devono esserci un documento e una proposta per risolverlo in C++.

Il comitato ISO ha considerato la proposta P0478, ma è stata respinta. Ci sono alcune altre idee, ad esempio, vedere Parametri del modello variadico non terminale | cor3ntin ma senza le “materializzazioni” finali.

Sembra che dobbiamo aspettare alcuni anni e alcuni documenti per risolvere questo problema. Ma siccome non è urgente e ci sono altre soluzioni, forse è meglio non complicare ulteriormente il C++.

Riepilogo

Il teorema fondamentale dell'ingegneria del software (FTSE) (vedi @wiki):

La frase sopra descrive perfettamente ciò che ho mostrato in questo post del blog :) Poiché C++ non supporta argomenti variadici non terminali, abbiamo bisogno di un altro livello per risolverlo.

Ecco un riepilogo di tutte le tecniche:

Tecnica Pro Problemi
Diversi sovraccarichi Semplice numero limitato di parametri, non sembra "moderno".
Argomenti espliciti del modello Semplice Devi mantenere sincronizzato l'elenco di tipi e valori.
Guida alla detrazione Non c'è bisogno di menzionare i tipi, sembra una chiamata di funzione. Richiede il supporto C++17, più complicato da implementare. Crea un oggetto separato, anziché una semplice chiamata di funzione (ma forse sarà ottimizzato dal compilatore?). Non può restituire facilmente valori dal costruttore.
Struct + Costruttore + funzione Non è necessario menzionare i tipi, ma consente di restituire valori dalla funzione membro di registrazione. Crea un oggetto separato con uno stato, una sintassi più lunga.
Avvolgi in una tupla Relativamente facile Sembra strano? Devi aggiungere <tuple> intestazione.
Oggetto flusso Un approccio completamente nuovo, sembra semplice e simile a std::cout << chiamate. Più chiamate di funzione, necessita di un oggetto "globale" separato definito.

E qual è la tua opzione preferita?

Inoltre, dai un'occhiata al nostro altro articolo, che affronta un problema simile da un'altra prospettiva. Come passare un pacchetto Variadic come primo argomento di una funzione in C++ - Storie C++.

Come fonte per le tecniche, uso questa domanda SO:c++ - Come utilizzare source_location in una funzione modello variadic? - Stack Overflow e anche dai commenti che ho ricevuto nel post iniziale sulla registrazione - vedi @disqus.