21

Originally I like to use something like this:

(true?a:b).test()

instead of

(true?a.test():b.test())

in order to save typing time if the function has the same name, initially I thought it should be valid, but I found:

#include <stdio.h>
class A{
public:
    char test(){
        return 'A';
    }
};

class B{
public:
    char test(){
        return 'B';
    }
};

int main(){
    printf("%c\n",(false?A():B()).test());
    return 0;
}

cannot compile, but if B is subclass of A:

#include <stdio.h>
class A{
public:
    char test(){
        return 'A';
    }
};

class B : public A{
public:
    char test(){
        return 'B';
    }
};

int main(){
    printf("%c\n",(false?A():B()).test());
    return 0;
}

it can compile, why?

Niall
  • 30,036
  • 10
  • 99
  • 142
ggrr
  • 7,737
  • 5
  • 31
  • 53
  • Becuase the object you try to return must conform to the same interface, in your case it must implement `test` method. Compiler must be sure that in any case the object returned will be consistent with the interface. – mic4ael Jul 14 '15 at 07:21
  • 7
    because C++ doesn't really do dynamic dispatching of methods (unlike e.g. python). – umläute Jul 14 '15 at 07:28
  • 5
    Note that `true?a.test():b.test()` will also fail if there are no common_type for `test` results. – Jarod42 Jul 14 '15 at 09:49
  • 1
    Another interesting case where the conditional operator works is with captureless lambdas with compatible signatures after conversion to a function pointer see: [MSVC error when using capture-less lambda expressions as second and third operand of conditional operator](http://stackoverflow.com/q/27989031/1708801) – Shafik Yaghmour Jul 14 '15 at 13:22

4 Answers4

52

The reason is that (test?a:b) is an expression and must have a type. That type is the common type of a and b, and unrelated types have no type in common. The common type of a base and derived class is the base class.

Note that the question contains an assumption that the only case which compiles is where there's a common base type. In fact, it also compiles if there's a unambiguous conversion from one type to the other.

MSalters
  • 173,980
  • 10
  • 155
  • 350
  • 1
    If `A` and `B` both derive from `C`, will `test ? A() : B()` work? – BlueRaja - Danny Pflughoeft Jul 14 '15 at 14:47
  • According to this answer, yes (assuming the function `test()` is defined in `C` and overloaded in `A` and `B`). Think of the `(test?A():B())` notation as an alias for a function like `Type Iff(bool conditionExpression, Type trueResult, Type falseResult) { if (conditionExpression) return trueResult; else return falseResult; }`. Then calling `Iff(test, a, b).test();` makes more sense. – talrnu Jul 14 '15 at 16:40
  • @BlueRaja-DannyPflughoeft: Logical as that may sound, no. I dont& have Design&Evolution of C++ handy, but I assume it's because C++ has multiple inheritance. With single inheritance, it's a simple matter of finding the 2 roots of the inheritance chain, and comparing from there on to the point where the two chains diverge. With MI, you'd have to compare every root with every root, and deal with ambiguities. – MSalters Jul 15 '15 at 07:10
18

The conditional operator (?:) operands must have a common type. I.e. given E1 ? E2 : E3 then E2 and E3 must be unambiguously convertible. This type is then the return type that is then used for the expression as a whole.

From cppreference, they list the rules and requirements, but a salient line that is relevant here is;

The return type of a conditional operator is also accessible as the binary type trait std::common_type

Which basically is saying that there must be a common type, and std::common_type can be used to calculate that type.

Based on your code snippet (true ? a.test() : b.test()) worked since both a.test() and b.test() returned char. However a and b are unrelated thus cannot be used by themselves.


The relevant material in the C++ (WD n4527) standard is found is §5.16 ([expr.cond]). There are several rules and conversions that are applied, the gist of it is that if there is no conversion, or if the conversion is ambiguous, the program is ill-formed.

Niall
  • 30,036
  • 10
  • 99
  • 142
4

If the second and third operand to the conditional operator do not have the same "type" then an attempt to covert one of the operands to the other is made. If this conversion can not be made or is ambiguous then the program is ill-formed.

This has what to some may seem unexpected results, one of those interesting cases would be capturless lambda with compatible function signature after conversion to a function pointer, for example:

#include <iostream>
#include <algorithm>
#include <vector>

int main() {
    std::vector<int> v(0, 10);
    bool increase = true;
    std::sort(v.begin(), v.end(), increase ? 
          [](int lhs, int rhs){return lhs < rhs;} : 
          [](int lhs, int rhs){return lhs > rhs;} );
    return 0;
} 

is valid. We can see from the following bug report this also applies in general and in particular for this question applies to classes without a common base. The following example is taken from the bug report:

struct A{ typedef void (*F)(); operator F(); };
struct B{ typedef void (*F)(); operator F(); };

void f() {
  false ? A() : B();
}

is valid since like the captureless lambda case both A and B can be converted to a function pointer.

For reference the draft C++ standard in section 5.16 [expr.cond] says:

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.

and then covers the rules and then says:

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

Community
  • 1
  • 1
Shafik Yaghmour
  • 154,301
  • 39
  • 440
  • 740
0

Add it to the language:

template<class F>
struct if_t {
  bool b;
  F f;
  template<class Lhs, class Rhs>
  auto operator()(Lhs&&lhs, Rhs&&rhs)&&
  ->std::result_of_t<F(Lhs)>
  {
    if (b) return std::forward<F>(f)(std::forward<Lhs>(lhs));
    else return std::forward<F>(f)(std::forward<Rhs>(rhs));
  }
  template<class Lhs>
  void operator()(Lhs&&lhs)&&
  {
    if (b)
      std::forward<F>(f)(std::forward<Lhs>(lhs));
  }
};

template<class F>
if_t<std::decay_t<F>> branch(bool b, F&& f){
  return {b,std::forward<F>(f)};
}

then we get:

branch(false, [&](auto&&arg){return arg.test();})
(
  A{}, B{}
);

where it only works if both A and B have a .test() and the return value of B::test() can be converted into the return value of A::test().

Sadly, both A{} and B{} are constructed.

template<class T>
auto make(){return [](auto&&...args){return {decltype(args)(args)...};}}

branch(false, [&](auto&&arg){return arg().test();})
(
  make<A>(), make<B>()
);

which differs the construction.

Not the most elegant syntax. It can be cleaned up some, but not enough (in my opinion). There is no way to create lazy operators in C++ with a clean syntax, you can only use the built-in ones.

Anyhow, your code cannot work because ?: is an expression that returns a type. There is no type that can represent both an A and a B, so it cannot work. If one was the base of the other, or there was a conversion, etc then it would work.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524