Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does std::istringstream appear to resolve differently to std::ifstream in the ternary (?:) operator?

I am used to writing little command line tools that take either a file name or read from std::cin, so I have been using this pattern for quite a while:

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

After reading a question on Stack Overflow, I tried to modify my usual pattern to suit the need to read either from a file or from a std::istringstream. To my surprise it won't compile and gives this error:

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

Which looks to me like it's trying to convert the std::istringstream object (iss) to a boolean and getting its 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. Why is it treating std::istringstream differently from std::cin and std::ifstream? They all derive from std::istream.

    Then I remembered having converted my pattern to accommodate three possibilities, reading from file, string or std::cin. And I remember that worked (although it's pretty clumsy). So applying the triple solution to this problem I came up with a fudge that totally works:

    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. Why does this fudge work? Is GCC breaking any rules on how the ternary operator (?:) resolves its types? Or am I missing something?

like image 658
Galik Avatar asked Jul 11 '14 21:07

Galik


3 Answers

Minimized example:

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

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

Clang reports:

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

The relevant rule is found in §5.16 [expr.cond]/p3-6 of the standard:

3 Otherwise, if the second and third operand have different types and either has (possibly cv-qualified) class type, or if both are glvalues of the same value category and the same type except for cv-qualification, an attempt is made to convert each of those operands to the type of the other. The process for determining whether an operand expression E1 of type T1 can be converted to match an operand expression E2 of type T2 is defined as follows:

  • If E2 is an lvalue: E1 can be converted to match E2 if E1 can be implicitly converted (Clause 4) to the type “lvalue reference to T2”, subject to the constraint that in the conversion the reference must bind directly (8.5.3) to an lvalue.
  • If E2 is an xvalue: E1 can be converted to match E2 if E1 can be implicitly converted to the type “rvalue reference to T2”, subject to the constraint that the reference must bind directly.
  • If E2 is a prvalue or if neither of the conversions above can be done and at least one of the operands has (possibly cv-qualified) class type:
    • if E1 and E2 have class type, and the underlying class types are the same or one is a base class of the other: E1 can be converted to match E2 if the class of T2 is the same type as, or a base class of, the class of T1, and the cv-qualification of T2 is the same cv-qualification as, or a greater cv-qualification than, the cv-qualification of T1. If the conversion is applied, E1 is changed to a prvalue of type T2 by copy-initializing a temporary of type T2 from E1 and using that temporary as the converted operand.
    • Otherwise (i.e., if E1 or E2 has a nonclass type, or if they both have class types but the underlying classes are not either the same or one a base class of the other): E1 can be converted to match E2 if E1 can be implicitly converted to the type that expression E2 would have if E2 were converted to a prvalue (or the type it has, if E2 is a prvalue).

Using this process, it is determined whether the second operand can be converted to match the third operand, and whether the third operand can be converted to match the second operand. If both can be converted, or one can be converted but the conversion is ambiguous, the program is ill-formed. If neither can be converted, the operands are left unchanged and further checking is performed as described below. If exactly one conversion is possible, that conversion is applied to the chosen operand and the converted operand is used in place of the original operand for the remainder of this section.

4 If the second and third operands are glvalues of the same value category and have the same type, the result is of that type and value category and it is a bit-field if the second or the third operand is a bit-field, or if both are bit-fields.

5 Otherwise, the result is a prvalue. If the second and third operands do not have the same type, and either has (possibly cv-qualified) class type, overload resolution is used to determine the conversions (if any) to be applied to the operands (13.3.1.2, 13.6). If the overload resolution fails, the program is ill-formed. Otherwise, the conversions thus determined are applied, and the converted operands are used in place of the original operands for the remainder of this section.

6 Lvalue-to-rvalue (4.1), array-to-pointer (4.2), and function-to-pointer (4.3) standard conversions are performed on the second and third operands. After those conversions, one of the following shall hold:

  • The second and third operands have the same type; the result is of that type. If the operands have class type, the result is a prvalue temporary of the result type, which is copy-initialized from either the second operand or the third operand depending on the value of the first operand.
  • The second and third operands have arithmetic or enumeration type; the usual arithmetic conversions are performed to bring them to a common type, and the result is of that type.
  • One or both of the second and third operands have pointer type; pointer conversions (4.10) and qualification conversions (4.4) are performed to bring them to their composite pointer type (Clause 5). The result is of the composite pointer type.
  • One or both of the second and third operands have pointer to member type; pointer to member conversions (4.11) and qualification conversions (4.4) are performed to bring them to their composite pointer type (Clause 5). The result is of the composite pointer type.
  • Both the second and third operands have type std::nullptr_t or one has that type and the other is a null pointer constant. The result is of type std::nullptr_t.

The crucial point is that this will always attempt to convert one operand to match the type of the other, rather than convert both to a third type, until you hit paragraph 5, at which point the compiler starts looking for user-defined implicit conversions to pointer or arithmetic types (those are only possible arguments to the built-in candidate functions for operator?: defined in §13.6), and for your purposes, you really don't want it to get there.

In the minimized example, which correlates directly to your error case (A = istream, B = ifstream, C = istringstream), converting one to the type of the other is not possible, and so the logic goes down to p5, and the compiler looks for user-defined implicit conversions. In the minimized example there's no conversion, overload resolution fails, and the whole thing is ill-formed. In your error case, pre-C++11 (and in libstdc++ post-C++11, apparently) there's an implicit conversion from a stream to void *, so the compiler does that, giving the whole expression a void * type, but that obviously can't bind to a reference to std::istream, so that's the error you see.

In your second case:

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

std::cin has type std::istream, and std::istringstream can be converted to its base class std::istream, so the inner conditional expression is well-formed and has type std::istream. Then with the outer conditional expression, again the type of the second operand, std::ifstream, is convertible to the type of the third operand, std::istream, so the entire expression is well-formed and has the right type to bind to the reference.

like image 186
T.C. Avatar answered Oct 22 '22 01:10

T.C.


If you have a base class and a derived class, the ternary conditional operator knows to convert the derived class to the base class. But if you have two derived classes, it doesn't know to convert them to their common base class. This isn't gcc acting up; this is just how the ternary conditional operator is specified to work in the standard.

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

This works fine because std::cin has type std::istream, which is a base class of std::ifstream.

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

This doesn't work because std::ifstream and std::istringstream "only" have a common base class.

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

This works because it is parsed as:

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

and the parenthesized expression has type std::istream. So iss is converted into an lvalue of type std::istream, if selected, and ifs is also converted similarly.

like image 14
Brian Bi Avatar answered Oct 22 '22 01:10

Brian Bi


The compiler tries to find a common type for both result from the ternary operator, and if you see e.g. this reference you will see there is a casting operator override for void* (or bool for C++11 and later), so the compiler uses that.

But then when it tries to do the assignment, it errors out because on the right-hand side of the initialization you have a void* (alternatively bool) type, and on the left-hand side there's a reference to std::istream.

To solve it you have to manually cast each stream to a reference to std::istream with e.g. static_cast.

like image 6
Some programmer dude Avatar answered Oct 22 '22 00:10

Some programmer dude