Подтвердить что ты не робот

Почему std:: istringstream выглядит по-разному в std:: ifstream в тройном (?:) операторе?

Я привык писать небольшие инструменты командной строки, которые принимают либо имя файла, либо читаются с std::cin, поэтому я довольно долго использовал этот шаблон:

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

После прочтения вопроса о переполнении стека, я попытался изменить свой обычный шаблон в соответствии с необходимостью читать либо из файла, либо из std::istringstream. К моему удивлению, он не будет компилироваться и дает эту ошибку:

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

Мне кажется, что он пытается преобразовать объект std::istringstream (iss) в boolean и получить его 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;
}
  • Почему он обрабатывает std::istringstream иначе, чем std::cin и std::ifstream? Все они происходят от std::istream.

    Затем я вспомнил, что преобразовал свой шаблон, чтобы разместить три возможности: чтение из файла, строки или std::cin. И я помню, что работал (хотя и довольно неуклюжий). Поэтому, применяя тройное решение этой проблемы, я придумал выдумку, которая полностью работает:

    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;
    }
    
  • Почему эта выдумка работает? Является ли GCC нарушением каких-либо правил о том, как тернарный оператор (?:) разрешает его типы? Или я что-то упускаю?

4b9b3361

Ответ 1

Минимизированный пример:

class A { };
class B : public A { };
class C : public A { };

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

Отчеты Clang:

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

Соответствующее правило содержится в §5.16 [expr.cond]/p3-6 стандарта:

3 В противном случае, если второй и третий операнды имеют разные типы и либо имеет (возможно, cv-qualified) тип класса, либо если оба являются значениями glvalues той же категории значений и того же типа, за исключением cv-qualification, делается попытка преобразовать каждый из этих операндов к типу другого. Процесс определения того, выражение операнда E1 типа T1 может быть преобразовано в соответствии с операндом выражение E2 типа T2 определяется следующим образом:

  • Если E2 является lvalue: E1 может быть преобразован в соответствие E2, если E1 может быть неявно преобразован (раздел 4) в тип "lvalue reference to T2", при условии, что при преобразовании ссылка должна свяжите непосредственно (8.5.3) с lvalue.
  • Если E2 является значением x: E1 может быть преобразован в соответствие E2, если E1 может быть неявно преобразован в тип "rvalue reference to T2", при условии, что ограничение, которое ссылка должна связывать напрямую.
  • Если E2 является prvalue или если ни одно из приведенных выше преобразований не может быть выполнено, и хотя бы один из операндов имеет (возможно, cv-квалификацию) Тип класса:
    • если E1 и E2 имеют тип класса, а базовые типы классов одинаковы или один является базовым классом другого: E1 может быть преобразован в соответствие E2, если класс T2 является тем же типом, что и базовый класс, класс T1 и cv-квалификация T2 одинаковы cv-qualification как, или более высокая cv-квалификация, чем cv-квалификация T1. Если применяется преобразование, E1 изменяется на prvalue типа T2 путем копирования-инициализации временного типа T2 из E1 и использовать это временное значение как преобразованный операнд.
    • В противном случае (т.е. если E1 или E2 имеет неклассический тип, или если оба они имеют типы классов, но базовые классы не являются одинаковыми или один базовый класс другого): E1 можно преобразовать в соответствие E2, если E1 могут быть неявно преобразованы в тип, который будет иметь выражение E2 если E2 были преобразованы в prvalue (или тип, который он имеет, если E2 является prvalue).

Используя этот процесс, определяется, может ли второй операнд быть преобразуется в соответствие с третьим операндом и является ли третий операнд могут быть преобразованы в соответствии со вторым операндом. Если оба могут быть преобразованный, или можно преобразовать, но преобразование неоднозначно, программа плохо сформирована. Если ни один из них не может быть преобразован, операнды остаются без изменений, и дальнейшая проверка выполняется, как описано ниже. Если возможно одно преобразование, это преобразование применяется к выбранному операнду, а преобразованный операнд используется в место исходного операнда для остальной части этого раздела.

4 Если второй и третий операнды являются glvalues ​​одного значения категории и имеют один и тот же тип, результат такого типа и значения и это бит-поле, если второй или третий операнд является бит-поле, или если оба являются битовыми полями.

5 В противном случае результатом будет prvalue. Если второй и третий операнды не имеют одного и того же типа и имеют (возможно, cv-квалификацию) тип класса, разрешение перегрузки используется для определения конверсий (если таковые имеются) для применения к операндам (13.3.1.2, 13.6). Если ошибка перегрузки не работает, программа плохо сформирована. В противном случае таким образом определяются определенные преобразования, а преобразованные операнды используются вместо исходных операндов для остальной части этого раздел.

6 Lvalue-to-rvalue (4.1), массив-указатель (4.2) и стандартное преобразование функции-to-pointer (4.3) выполняется на второго и третьего операндов. После этих преобразований один из следующее:

  • Второй и третий операнды имеют один и тот же тип; результат такого типа. Если операнды имеют тип класса, результатом является значение prvalue временный тип результата, который инициализируется копированием из второй операнд или третий операнд в зависимости от значения первый операнд.
  • Второй и третий операнды имеют тип арифметики или перечисления; обычные арифметические преобразования выполняются, чтобы привести их к общий тип, и результат этого типа.
  • Один или оба из второго и третьего операндов имеют тип указателя; конверсии указателей (4.10) и преобразования квалификации (4.4) являются чтобы привести их к их составному указателю (раздел 5). Результат - составной тип указателя.
  • Один или оба из второго и третьего операндов имеют указатель на тип члена; указатель на преобразования членов (4.11) и квалификацию преобразования (4.4) выполняются, чтобы привести их к их составным тип указателя (пункт 5). Результат - составной тип указателя.
  • Оба второго и третьего операндов имеют тип std::nullptr_t или один имеет этот тип, а другой - константу нулевого указателя. В результате типа std::nullptr_t.

Важнейшим моментом является то, что это всегда будет пытаться преобразовать один операнд в соответствие с типом другого, а не конвертировать оба в третий тип, пока вы не нажмете на пункт 5, после чего компилятор начнет искать пользовательские неявные преобразования в указатели или арифметические типы (это только возможные аргументы для встроенных функций-кандидатов для operator?:, определенных в §13.6), и для ваших целей вы действительно не хотите, чтобы он туда попал.

В свернутом примере, который напрямую коррелирует с вашим случаем ошибки (A= istream, B= ifstream, C= istringstream), преобразование одного в тип другого невозможно, и поэтому логика переходит на p5, и компилятор ищет пользовательские неявные преобразования. В минимизированном примере нет конверсии, разрешение перегрузки не выполняется, и все это плохо сформировано. В вашем случае ошибки pre-С++ 11 (и в libstdС++ post-С++ 11, по-видимому) есть неявное преобразование из потока в void *, поэтому компилятор делает это, давая целое выражение a void * тип, но это, очевидно, не может привязываться к ссылке на std::istream, так что вы видите ошибку.

В вашем втором случае:

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

std::cin имеет тип std::istream, а std::istringstream может быть преобразован в его базовый класс std::istream, поэтому внутреннее условное выражение хорошо сформировано и имеет тип std::istream. Затем с внешним условным выражением снова тип второго операнда std::ifstream преобразуется в тип третьего операнда std::istream, поэтому все выражение хорошо сформировано и имеет правильный тип для привязки к ссылка.

Ответ 2

Если у вас есть базовый класс и производный класс, тернарный условный оператор знает, чтобы преобразовать производный класс в базовый класс. Но если у вас есть два производных класса, он не знает, чтобы преобразовать их в свой общий базовый класс. Это не gcc, действующий; это то, как указано тернарный условный оператор для работы в стандарте.

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

Это работает отлично, потому что std::cin имеет тип std::istream, который является базовым классом std::ifstream.

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

Это не работает, потому что std::ifstream и std::istringstream "only" имеют общий базовый класс.

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

Это работает, потому что он анализируется как:

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

и выражение в скобках имеет тип std::istream. Таким образом, iss преобразуется в lvalue типа std::istream, если выбрано, и ifs также преобразуется аналогично.

Ответ 3

Компилятор пытается найти общий тип для обоих результатов от тернарного оператора, и если вы видите, например, эта ссылка вы увидите переопределение оператора для void* (или bool для С++ 11 и позже), поэтому компилятор использует это.

Но тогда, когда он пытается выполнить назначение, он ошибочно, потому что в правой части инициализации есть тип void* (альтернативно bool), а в левой части есть ссылка до std::istream.

Чтобы решить эту проблему, вам необходимо вручную включить каждый поток в ссылку на std::istream, например. static_cast.

Ответ 4

gcc жалуется, потому что ifs и iss - два разных типа. static_casting типы для std:: istream & решает вашу проблему.