为什么(false?A():B()).test()只有在A和B有子类关系时才能编译

Why can (false?A():B()).test() compile only when A and B have a subclass relationship?

本文关键字:子类 关系 编译 false test 为什么      更新时间:2023-10-16

最初我喜欢这样使用:

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

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

为了节省输入时间,如果函数具有相同的名称,最初我认为它应该是有效的,但我发现:

#include <stdio.h>
class A{
public:
    char test(){
        return 'A';
    }
};
class B{
public:
    char test(){
        return 'B';
    }
};
int main(){
    printf("%cn",(false?A():B()).test());
    return 0;
}

不能编译,但如果BA的子类:

#include <stdio.h>
class A{
public:
    char test(){
        return 'A';
    }
};
class B : public A{
public:
    char test(){
        return 'B';
    }
};
int main(){
    printf("%cn",(false?A():B()).test());
    return 0;
}

可以编译,为什么?

因为(test?a:b)是一个表达式,必须有一个类型。该类型是a和b的共同类型,而不相关的类型没有共同类型。基类和派生类的共同类型是基类。

请注意,问题包含一个假设,即编译的only情况是存在公共基类型的地方。事实上,如果存在从一种类型到另一种类型的明确转换,它也会编译。

条件运算符(?:)操作数必须具有共同的类型。即给定E1 ? E2 : E3,则E2E3必须是明确可转换的。该类型然后作为返回类型用于整个表达式。

从cppreference中,他们列出了规则和需求,但这里重要的一行是;

条件操作符的返回类型也可以作为二进制类型trait std::common_type

访问。

这基本上是说必须有一个共同的类型,std::common_type可以用来计算该类型。

基于你的代码片段(true ? a.test() : b.test())工作,因为a.test()b.test()返回char。但是ab是不相关的,因此不能单独使用。


c++ (WD n4527)标准中的相关资料见§5.16 ([expr.cond])。这里应用了几个规则和转换,其要点是如果没有转换,或者转换是不明确的,则程序是病态的

如果条件运算符的第二个和第三个操作数不具有相同的"类型",则尝试将其中一个操作数转换为另一个操作数。如果不能进行这种转换或有歧义,则程序是病态的。

这会产生一些意想不到的结果,其中一个有趣的例子是在转换为函数指针后具有兼容函数签名的无捕获lambda,例如:

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

是有效的。我们可以从下面的bug报告中看到,这也适用于一般情况,特别是对于这个问题,适用于没有公共基的类。下面的例子摘自bug报告:

struct A{ typedef void (*F)(); operator F(); };
struct B{ typedef void (*F)(); operator F(); };
void f() {
  false ? A() : B();
}

是有效的,因为AB都可以转换为函数指针,就像无捕获的lambda情况一样。

参考5.16 [expr]章节中的c++标准草案。气孔导度]说:

否则,如果第二个和第三个操作数具有不同的类型,并且其中一个具有(可能是cv限定的)类类型,或者如果两者都是相同值类别和相同类型的值(cv-qualification除外),则使用将尝试将其中每个操作数转换为另一个操作数的类型。

然后涵盖规则,然后说:

使用此过程,确定第二个操作数是否可以转换为与第三个操作数匹配操作数,以及是否可以将第三个操作数转换为匹配第二个操作数。如果两者都可以转换,或者可以转换,但转换是不明确的,程序是病态的。如果既不可以转换,操作数保持不变,并按如下所述执行进一步的检查。如果只能进行一次转换,则将该转换应用于所选操作数和转换后的对象

部分的剩余部分使用操作数代替原始操作数。

添加到语言中:

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

则得到:

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

,只有当AB都有一个.test(),并且B::test()的返回值可以转换为A::test()的返回值时才有效。

遗憾的是,A{}B{}都是构造的。

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

不同的结构

不是最优雅的语法。它可以被清理一些,但还不够(在我看来)。在c++中没有办法用干净的语法创建惰性操作符,你只能使用内置的。

无论如何,你的代码不能工作,因为?:是一个返回类型的表达式。没有可以同时表示AB的类型,所以它不能工作。如果一个是另一个的基底,或者有转换等,那么它就可以工作。