当使用std::function时,调用运算符会选择自动返回类型而不是构造函数。

18
以下代码片段:
#include <functional>

struct X {
    X(std::function<double(double)> fn); // (1)
    X(double, double);                   // (2)

    template <class T>
    auto operator()(T const& t) const {  // (3)
        return t.foo();
    }
};

int main() {
    double a, b;
    auto x = X(a, b);
    return 0;
}

在使用 -std=c++14 编译时,无论是在 OSX 还是在 godbolt.org 上测试,在 clang (4.0.1) 和 g++ (6.3, 7.2) 上都会编译失败。

但如果:

  • 我删除构造函数 (1);
  • 或者在 (3) 中将返回类型设置为具体类型(例如 double);
  • 或者在 (3) 中使用后置返回类型 (-> decltype(t.foo())) ;
  • 使用 -std=c++1z 编译(感谢 @bolov)。

也许我忽略了一些明显的问题...这段代码有什么问题吗?这是个bug吗?


没有使用C++1z标志的错误:https://godbolt.org/g/izbzye 但是,说真的,用C++14想干什么? - bolov
我猜 auto x = X{a, b}; 是另一种可以成功编译的语句变体? - Toby Speight
@TobySpeight 对我来说不行,它出现了相同的错误。 - Holt
5
在C++17中,规则已经改变(保证复制省略),代码等价于 X x(a, b);;在C++11中,推断的 auto 作为返回类型还没有得到支持。 - Jarod42
如果在C++17中添加代码行auto y = x,则会发生相同的情况。 - Oliv
1个回答

21
您正在进行拷贝初始化操作,初始化对象为 x。而 X 是一个棘手的类型,原因如下:
  1. 它可以由任何可调用对象构造而成,该对象可以使用双精度参数进行调用,并且其返回类型可转换为双精度。

  2. 它本身是一个可调用对象,可以使用双精度参数进行调用。但需要推导返回类型。

  3. X 有一个由编译器生成的复制构造函数。

这里开始变得有些模糊了,希望您能意识到需要进行重载决议。

如果您移除第一个构造函数,那么明显地消除了歧义。有趣的情况在于函数调用运算符。

您会发现,只有当参数转换和返回类型之间的转换是可能的时,std::function 才能从所传递的可调用对象进行构造(模板化的构造函数将参与重载决议)。所有这些都是在未求值的上下文中完成的,因此模板化的函数调用运算符不会被 odr-used,因此也不会被实例化。

当返回类型是占位类型时,std::function 构造函数无法轻易地决定是否应该构造。在重载决议期间编译失败,即使成功了,也将选择 X 的复制构造函数。


正如 @VTT 在评论中建议的那样,将接受 std::function 的构造函数标记为显式 (explicit) 也将解决歧义。这是因为编译器不再需要评估隐式转换序列。


@bolov - 有两个可行的 c'tors 来构建对象。除非其中一个明确被取消资格。 - StoryTeller - Unslander Monica
5
将接受函数参数的构造函数标记为explicit可以使其正常工作。 - user7860670
4
如果您写成X x1(a, b); X x2 = x1;会更清楚,问题在于如何从x1构造x2 - Holt
2
@StoryTeller 谢谢,我确实理解了问题的根源,但我不明白它为什么会导致这种情况 - 复制构造函数难道不是更好的匹配吗?如果是这样,为什么会实例化 std::function<double(double)>(X) 的构造函数呢? - Holt
3
@Holt - 是的,但问题在于仍然需要进行重载决议,并且占位符类型会干扰检查应该被取消资格的 std::function - StoryTeller - Unslander Monica
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接