Perché std :: istringstream sembra risolversi in modo diverso da std :: ifstream nel ternario (? :) operator?

Sono abituato a scrivere piccoli strumenti da riga di comando che prendono o il nome di un file o letti da std::cin , quindi uso questo pattern da un po ‘di tempo:

 int main(int argc, char* argv[]) { std::string filename; // args processing ... std::ifstream ifs; if(!filename.empty()) ifs.open(filename); std::istream& is = ifs.is_open() ? ifs : std::cin; std::string line; while(std::getline(is, line)) { // process line... } return 0; } 

Dopo aver letto una domanda su Stack Overflow, ho provato a modificare il mio solito schema per adattarlo alla necessità di leggere da un file o da uno std::istringstream . Con mia sorpresa, non verrà compilato e restituirà questo errore:

 temp.cpp:164:47: error: invalid initialization of non-const reference of type 'std::istream& {aka std::basic_istream&}' from an rvalue of type 'void*' std::istream& is = ifs.is_open() ? ifs : iss; // won't compile 

A me sembra che std::istringstream cercando di convertire l’object std::istringstream ( iss ) in un booleano e ottenere il suo operator void*() .

 int main(int argc, char* argv[]) { std::string filename; std::string data; // args processing ... std::ifstream ifs; std::istringstream iss; if(!filename.empty()) ifs.open(filename); std::istream& is = ifs.is_open() ? ifs : iss; // won't compile std::string line; while(std::getline(is, line)) { // process line... } return 0; } 
  1. Perché tratta std::istringstream diverso da std::cin e std::ifstream ? Derivano tutti da std::istream .

    Poi mi sono ricordato di aver convertito il mio pattern per adattarlo a tre possibilità, leggendo da file, string o std::cin . E ricordo che ha funzionato (anche se è piuttosto maldestro). Quindi, applicando la tripla soluzione a questo problema, mi è venuto in mente un fudge che funziona totalmente:

     int main(int argc, char* argv[]) { std::string filename; std::string data; // args processing ... std::ifstream ifs; std::istringstream iss; if(!filename.empty()) ifs.open(filename); std::istream& is = ifs.is_open() ? ifs : true ? iss : std::cin; // fudge works std::string line; while(std::getline(is, line)) { // process line... } return 0; } 
  2. Perché questo fondente funziona? GCC sta violando le regole su come l’operatore ternario ( ?: 🙂 Risolve i suoi tipi? O mi sta sfuggendo qualcosa?

Esempio minimizzato:

 class A { }; class B : public A { }; class C : public A { }; int main() { B b; C c; A& refA = true? b : c; } 

Rapporti clang:

 main.cpp:13:19: error: incompatible operand types ('B' and 'C') A& refA = true? b : c; 

La regola rilevante si trova in §5.16 [expr.cond] / p3-6 dello standard:

3 Altrimenti, se il secondo e il terzo operando hanno tipi diversi e hanno un tipo di class (eventualmente qualificata per il cv), o se entrambi sono glival della stessa categoria di valore e dello stesso tipo eccetto per la qualifica cv, viene effettuato un tentativo di conversione ciascuno di questi operandi al tipo dell’altro. Il processo per determinare se un’espressione di operando E1 di tipo T1 può essere convertita per corrispondere a un’espressione di operando E2 di tipo T2 è definita come segue:

  • Se E2 è un lvalue: E1 può essere convertito per corrispondere a E2 se E1 può essere convertito implicitamente (Clausola 4) al tipo “lvalue reference to T2”, sobject al vincolo che nella conversione il riferimento deve legarsi direttamente (8.5.3 ) a un lvalue.
  • Se E2 è un valore x: E1 può essere convertito per corrispondere a E2 se E1 può essere convertito implicitamente nel tipo “riferimento a T2”, sobject al vincolo che il riferimento deve bind direttamente.
  • Se E2 è un valore nominale o se nessuna delle conversioni precedenti può essere eseguita e almeno uno degli operandi ha un tipo di class (eventualmente qualificato cv):
    • se E1 ed E2 hanno un tipo di class e i tipi di class sottostanti sono uguali o uno è una class base dell’altro: E1 può essere convertito per corrispondere a E2 se la class di T2 è dello stesso tipo di, o una class base di, la class di T1 e la qualifica di cv di T2 è la stessa qualifica di cv o una qualifica di cv maggiore rispetto alla qualifica di cv di T1. Se la conversione viene applicata, E1 viene modificato in un valore di proto di tipo T2 mediante l’inizializzazione della copia di un temporaneo di tipo T2 da E1 e utilizzando tale temporaneo come operando convertito.
    • Altrimenti (cioè, se E1 o E2 ha un tipo non class, o se entrambi hanno tipi di class ma le classi sottostanti non sono uguali o una class base dell’altro): E1 può essere convertito per corrispondere a E2 se E1 può essere implicitamente convertito nel tipo che l’espressione E2 avrebbe se E2 fosse convertito in un valore di prvalue (o il tipo che ha, se E2 è un valore nominale).

Usando questo processo, viene determinato se il secondo operando può essere convertito per corrispondere al terzo operando e se il terzo operando può essere convertito per corrispondere al secondo operando. Se entrambi possono essere convertiti, o uno può essere convertito ma la conversione è ambigua, il programma è mal formato. Se nessuno dei due può essere convertito, gli operandi rimangono invariati e viene effettuato un ulteriore controllo come descritto di seguito. Se è ansible eseguire esattamente una conversione, tale conversione viene applicata all’operando scelto e l’operando convertito viene utilizzato al posto dell’operando originale per il resto di questa sezione.

4 Se il secondo e il terzo operando sono glival della stessa categoria di valore e hanno lo stesso tipo, il risultato è di quel tipo e categoria di valore ed è un campo di bit se il secondo o il terzo operando è un campo di bit, o se entrambi sono campi di bit.

5 Altrimenti, il risultato è un valore. Se il secondo e il terzo operando non hanno lo stesso tipo e hanno un tipo di class (eventualmente qualificata per il cv), viene utilizzata la risoluzione di sovraccarico per determinare le conversioni (se presenti) da applicare agli operandi (13.3.1.2, 13.6) . Se la risoluzione di sovraccarico fallisce, il programma è mal formato. Altrimenti, vengono applicate le conversioni così determinate e gli operandi convertiti vengono utilizzati al posto degli operandi originali per il resto di questa sezione.

6 Le conversioni standard da Lvalue a rvalue (4.1), array-to-pointer (4.2) e function-to-pointer (4.3) vengono eseguite sul secondo e sul terzo operando. Dopo tali conversioni, una delle seguenti deve contenere:

  • Il secondo e il terzo operando hanno lo stesso tipo; il risultato è di quel tipo. Se gli operandi hanno un tipo di class, il risultato è un valore provvisorio del tipo di risultato, che viene inizializzato dalla copia dal secondo operando o dal terzo operando in base al valore del primo operando.
  • Il secondo e il terzo operando hanno un tipo aritmetico o di enumerazione; le solite conversioni aritmetiche vengono eseguite per portarle a un tipo comune e il risultato è di quel tipo.
  • Uno o entrambi i secondi e terzi operandi hanno il tipo di puntatore; le conversioni puntatore (4.10) e le conversioni di qualificazione (4.4) vengono eseguite per portarle al loro tipo di puntatore composito (clausola 5). Il risultato è del tipo di puntatore composito.
  • Uno o entrambi i secondi e terzi operandi hanno un puntatore al tipo di membro; il puntatore alle conversioni di membro (4.11) e le conversioni di qualificazione (4.4) vengono eseguite per portarle al loro tipo di puntatore composito (clausola 5). Il risultato è del tipo di puntatore composito.
  • Sia il secondo che il terzo operando hanno tipo std::nullptr_t o uno ha quel tipo e l’altro è una costante puntatore nullo. Il risultato è di tipo std::nullptr_t .

Il punto cruciale è che questo cercherà sempre di convertire un operando in modo che corrisponda al tipo dell’altro, piuttosto che convertirlo in un terzo tipo, finché non si preme il paragrafo 5, a quel punto il compilatore inizia a cercare conversioni implicite definite dall’utente in i tipi di puntatori o aritmetici (quelli sono solo argomenti possibili per le funzioni candidate predefinite per l’ operator?: definite in §13.6), e per i tuoi scopi, davvero non vuoi che ci arrivi.

Nell’esempio ridotto, che si correla direttamente al tuo caso di errore ( A = istream , B = ifstream , C = istringstream ), la conversione di uno nel tipo dell’altro non è ansible, e quindi la logica scende a p5 e il compilatore cerca conversioni implicite definite dall’utente. Nell’esempio ridotto al minimo non c’è conversione, la risoluzione del sovraccarico non riesce e l’intera cosa è mal formata. Nel tuo caso di errore, pre-C ++ 11 (e in libstdc ++ post-C ++ 11, apparentemente) c’è una conversione implicita da un stream a void * , quindi il compilatore lo fa, dando all’intera espressione un tipo void * , ma ovviamente non è ansible associare a un riferimento a std::istream , quindi questo è l’errore che si vede.

Nel tuo secondo caso:

 ifs.is_open() ? ifs : true ? iss : std::cin; 

std::cin ha tipo std::istream , e std::istringstream può essere convertito nella sua class base std::istream , quindi l’espressione condizionale interna è ben strutturata e ha tipo std::istream . Quindi con l’espressione condizionale esterna, di nuovo il tipo del secondo operando, std::ifstream , è convertibile al tipo del terzo operando, std::istream , quindi l’intera espressione è ben formata e ha il tipo giusto per bind al riferimento.

Se si dispone di una class base e una class derivata, l’operatore condizionale ternario sa convertire la class derivata nella class base. Ma se hai due classi derivate, non sa come convertirle nella loro class base comune. Questo non è gcc che agisce; questo è il modo in cui l’operatore condizionale ternario è specificato per funzionare nello standard.

 std::istream& is = ifs.is_open() ? ifs : std::cin; 

Funziona bene perché std::cin ha tipo std::istream , che è una class base di std::ifstream .

 std::istream& is = ifs.is_open() ? ifs : iss; // won't compile 

Questo non funziona perché std::ifstream e std::istringstream “only” hanno una class base comune.

 std::istream& is = ifs.is_open() ? ifs : true ? iss : std::cin; // fudge works 

Funziona perché è analizzato come:

 std::istream& is = ifs.is_open() ? ifs : (true ? iss : std::cin); 

e l’espressione tra parentesi ha tipo std::istream . Quindi iss viene convertito in un lvalue di tipo std::istream , se selezionato, e ifs viene anche convertito in modo simile.

Il compilatore cerca di trovare un tipo comune per entrambi i risultati dall’operatore ternario e, se vedi questo riferimento , vedrai che c’è un override dell’operatore di casting per void* (o bool per C ++ 11 e successivi) , quindi il compilatore lo usa

Ma poi, quando cerca di eseguire il compito, sbaglia perché sul lato destro dell’inizializzazione si ha un tipo void* (in alternativa bool ), e sul lato sinistro c’è un riferimento a std::istream .

Per risolverlo devi lanciare manualmente ogni stream su un riferimento a std::istream con ad esempio static_cast .

gcc si lamenta perché ifs e iss sono due tipi diversi. static_casting i tipi di std :: istream e risolverà il tuo problema.