Why Does Std::Istringstream Appear to Resolve Differently to Std::Ifstream in the Ternary (:) Operator

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

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.

Why does Op that captures rvalue of std::istringstream by value fails the Accumulateable concept?

Functors provided to all algorithms must be copyable. IOstream types are not copyable; they're move-only. And therefore, any lambda that contains them is not copyable.

Besides, you may as well be creating the istringstream inside the functor; clearing it and inserting a new string isn't exactly cheaper than constructing/destructing them in each cycle.

Initializing reference to istream

The problem with your code is following:

Your left-hand side of the ternary operator is a temporary (rvalue). However, your right hand-side is an lvalue (cin is an lvalue). As a result, compiler is trying to create a temporary out of cin, and fails because of copy constructor being not available.

As for the sultions - you can simply replace rdbuf() of cin with rdbuf() of your file, and use cin everywhere.


Here's the ultimate solution OP came up with:

ifstream file;
std::streambuf* old_cin_buf = cin.rdbuf(); // Store the old value
if (FileIsProvided())
{
file.open(args[1]);
old_cin_buf = cin.rdbuf(file.rdbuf()); // Replace the ReadBuffer on cin.
// Store the previous value as well.
}
// Use cin for all operations now. It will either use the File or StdIn as appropriate.
...
// Restore the original value, in case it was changed by using a file.
cin.rdbuf(old_cin_buf); // This is better be done before file object here goes out of scope

Incompatible operand types when using ternary conditional operator

C++'s type system determines expressions' types from the inside out[1]. That means that the conditional expression's type is determined before the assignment to CardAbility* is made, and the compiler has to choose with only CardAbilityBurn* and CardAbilityEmpty*.

As C++ features multiple inheritance and some more possible conversion paths, since none of the types is a superclass of the other the compilation stops there.

To compile successfully, you need to provide the missing part : cast one or both operands to the base class type, so the conditional expression as a whole can take that type.

auto* cardAbility = contains
? static_cast<CardAbility*>(new CardAbilityBurn(i))
: static_cast<CardAbility*>(new CardAbilityEmpty );

(Note the use of auto, since you already provided the destination type in the right-side expression.)

It is however a bit convoluted, so in the end the if-else structure is better-suited in this case.

[1] There is one exception : overloaded function names have no definitive type until you convert them (implicitly or explicitly) to one of their versions.

Is there a good idiom to deal with alternative output streams?

My preference is to use streams with suitable stream buffers installed. Here is one way direct output to a file or to std::cout:

#include <iostream>
#include <fstream>
int main(int ac, char* av) {
std::ofstream ofs;
if (1 < ac) {
ofs.open(av[1]);
// handle errors opening the file here
}
std::ostream os(file? file.rdbuf(): std::cout.rdbuf());
// use os ...
}


Related Topics



Leave a reply



Submit