std::函数的复制构造函数是否要求模板类型的参数类型是完整的类型

Does std::function's copy-constructor require the template type's argument types to be complete types?

本文关键字:类型 参数 复制 函数 构造函数 是否 std      更新时间:2023-10-16

给定:

#include <functional>
class world_building_gun;
class tile_bounding_box;
typedef std::function<void (world_building_gun, tile_bounding_box)> worldgen_function_t;
void foo() {
    worldgen_function_t v;
    worldgen_function_t w(v);
}

这应该编译吗? 我的编译器说:

是:GCC

/stdlibc++(在GCC和Clang中也是boost::function是肯定的)

否:Clang/libc++ (http://libcxx.llvm.org/,Clang 3.0,libc++ SVN,截至今天)

(如果"否"是正确的答案,我将修复我的真实代码以将完整类型放入更多标头或使用 boost::function。

编辑:这是Clang错误消息:

In file included from foo.cpp:2:
In file included from /usr/include/c++/v1/functional:462:
/usr/include/c++/v1/type_traits:2766:19: error: invalid appli
    static_assert(sizeof(_Tp) > 0, "Type must be complete.");
                  ^~~~~~~~~~~
/usr/include/c++/v1/type_traits:2752:15: note: in instantiation of template class 'std::__1::__check_complete<world_buildin
    : private __check_complete<_Hp>,
              ^
/usr/include/c++/v1/type_traits:2753:15: note: in instantiation of template class 'std::__1::__check_complete<world_buildin
      private __check_complete<_T0, _Tp...>
              ^
/usr/include/c++/v1/type_traits:2919:15: note: in instantiation of template class 'std::__1::__check_complete<std::__1::fun
      world_building_gun, tile_bounding_box>' requested here
    : private __check_complete<_Fp, _Args...>
              ^
/usr/include/c++/v1/type_traits:2930:11: note: in instantiation of template class 'std::__1::__invokable_imp<std::__1::func
      world_building_gun, tile_bounding_box>' requested here
          __invokable_imp<_Fp, _Args...>::value>
          ^
/usr/include/c++/v1/functional:1115:33: note: in instantiation of template class 'std::__1::__invokable<std::__1::function<
      world_building_gun, tile_bounding_box>' requested here
    template <class _Fp, bool = __invokable<_Fp&, _ArgTypes...>::value>
                                ^
/usr/include/c++/v1/functional:1141:35: note: in instantiation of default argument for '__callable<std::__1::function<void (world_building_gun, tile_bounding_box)> >' required here
              typename enable_if<__callable<_Fp>::value>::type* = 0);
                                  ^~~~~~~~~~~~~~~
/usr/include/c++/v1/functional:1140:7: note: while substituting deduced template arguments into function template 'function' [with _Fp = std::__1::function<void
      (world_building_gun, tile_bounding_box)>]
      function(_Fp,
      ^
foo.cpp:4:7: note: forward declaration of 'world_building_gun'
class world_building_gun;
      ^
In file included from foo.cpp:2:
In file included from /usr/include/c++/v1/functional:462:
/usr/include/c++/v1/type_traits:2766:19: error: invalid application of 'sizeof' to an incomplete type 'tile_bounding_box'
    static_assert(sizeof(_Tp) > 0, "Type must be complete.");
                  ^~~~~~~~~~~
/usr/include/c++/v1/type_traits:2753:15: note: in instantiation of template class 'std::__1::__check_complete<tile_bounding_box>' requested here
      private __check_complete<_T0, _Tp...>
              ^
/usr/include/c++/v1/type_traits:2753:15: note: in instantiation of template class 'std::__1::__check_complete<world_building_gun, tile_bounding_box>' requested here
      private __check_complete<_T0, _Tp...>
              ^
/usr/include/c++/v1/type_traits:2919:15: note: in instantiation of template class 'std::__1::__check_complete<std::__1::function<void (world_building_gun, tile_bounding_box)> &,
      world_building_gun, tile_bounding_box>' requested here
    : private __check_complete<_Fp, _Args...>
              ^
/usr/include/c++/v1/type_traits:2930:11: note: in instantiation of template class 'std::__1::__invokable_imp<std::__1::function<void (world_building_gun, tile_bounding_box)> &,
      world_building_gun, tile_bounding_box>' requested here
          __invokable_imp<_Fp, _Args...>::value>
          ^
/usr/include/c++/v1/functional:1115:33: note: in instantiation of template class 'std::__1::__invokable<std::__1::function<void (world_building_gun, tile_bounding_box)> &,
      world_building_gun, tile_bounding_box>' requested here
    template <class _Fp, bool = __invokable<_Fp&, _ArgTypes...>::value>
                                ^
/usr/include/c++/v1/functional:1141:35: note: in instantiation of default argument for '__callable<std::__1::function<void (world_building_gun, tile_bounding_box)> >' required here
              typename enable_if<__callable<_Fp>::value>::type* = 0);
                                  ^~~~~~~~~~~~~~~
/usr/include/c++/v1/functional:1140:7: note: while substituting deduced template arguments into function template 'function' [with _Fp = std::__1::function<void
      (world_building_gun, tile_bounding_box)>]
      function(_Fp,
      ^
foo.cpp:5:7: note: forward declaration of 'tile_bounding_box'
class tile_bounding_box;
      ^
2 errors generated.

如果我删除行"worldgen_function_t w(v);",或者如果我使类完成类型,Clang+libc++ 编译成功。

编辑:当然,这个问题现在已经修复,所以下面的文本可以看作是历史。 :)

<小时 />

问题确实(正如我所预测的那样)与模板化 ctor 中的 libc++ 的 SFINAE 检查有关(出于推理,请检查此问题)。它检查以下内容(例如)是否有效,并在施工现场给出一个漂亮而干净的错误,而不是在std::function的深处(尝试以下示例使用 libstd++ 或 MSVC...不寒而栗):

#include <functional>
void f(int* p){}
int main(){
  std::function<void(int)> fun(f);
}

libc++ 将导致编译器吐出类似"找不到与参数列表匹配的构造函数"之类的内容void (*)(int*),因为唯一适用的构造函数(模板化 ctor)会得到 SFINAE'd。

但是,为了使__callable__invoke_imp检查正常工作,参数和返回类型需要完整,否则此处不会考虑隐式转换。

甚至查看模板化 ctor 的原因是,在考虑最佳匹配(在本例中为复制 ctor)之前枚举所有 ctor。

<小时 />

现在,标准非常明确,当从可调用对象(也称为调用模板化 ctor )构造std::function对象时,参数和返回类型需要完整:

§20.8.11.2.1 [func.wrap.func.con] p7

template <class F> function(F f);
template <class F, class A> function(allocator_arg_t, const A& a, F f);

要求:FCopyConstructible。对于参数类型ArgTypes和返回类型 Rf应为可调用 (20.8.11.2)。[...]

(注意:"要求"针对功能的用户,而不是实现者。

§20.8.11.2 [func.wrap.func] p2

如果表达式INVOKE (f, declval<ArgTypes>()..., R)(被视为未计算的操作数(子句 5))格式正确 (20.8.2),则类型为 F 的可调用f对象对于参数类型ArgTypes和返回类型 R可调用的。

§20.8.2 [func.req]

p1 定义INVOKE (f, t1, t2, ..., tN)如下:

  • (t1.*f)(t2, ..., tN)f 是指向类T的成员函数的指针,并且t1是类型 T 的对象或对类型 T 对象的引用或对派生自 T 的类型对象的引用;
  • ((*t1).*f)(t2, ..., tN)f 是指向类T的成员函数的指针,并且t1不是上一项中描述的类型之一时;
  • [...]
  • f(t1, t2, ..., tN)在所有其他情况下。

p2 将INVOKE (f, t1, t2, ..., tN, R)定义为隐式转换为R INVOKE (f, t1, t2, ..., tN)

因此,libc++ 当然有权在模板化 ctor 中进行 SFINAE 检查,因为这些类型需要完整,否则你会得到未定义的行为。但是,即使不需要实际的 SFINAE 检查,完整类型的安全检查也会触发一个缺陷,这可能有点不幸,并且被认为是一个缺陷(因为将始终调用复制 ctor)。这可以通过使callable检查成为懒惰的检查来缓解,例如

template<bool Copy, class F>
struct lazy_callable{
  static bool const value = callable<F>::value;
};
template<class F>
struct lazy_callable<true, F>{
  static bool const value = false;
};
template<class F>
function(F f, typename enable_if<lazy_callable<!std::is_same<F,function>::value>::type* = 0);

这应该只触发callable SFINAE 检查,如果实际上没有std::function<...> F

伙计,我最后可能有点跑题了...

我已经提交了对libc ++的修复,以便此示例现在可以编译,提交修订160285。

我相信libc++在检查参数列表中的完整类型方面过于激进。

我会说不。从 20.8.11.2 类模板函数 [func.wrap.func] 开始,我们有:

3 function类模板是一个调用包装器 (20.8.1),其调用签名 (20.8.1) R(ArgTypes...)

在 20.8.1 定义 [func.def ] 中,我们得到了以下关于构成调用包装器类型、调用包装器和调用签名的定义:

2 调用签名是返回类型的名称,后跟以括号分隔的零个或多个参数类型的列表。

5 调用包装器类型是保存可调用对象并支持转发到该对象的调用操作的类型。

6 调用包装

器是调用包装器类型的对象。

请注意,第 2 段没有提到类型的完整性。

简而言之(涉及许多定义),这里的"可调用对象"的含义意味着函子(熟悉的概念,即可以像函数一样使用的东西)或指向成员的指针。此外,该标准还在20.8.11.2第2段中描述了可赎回的概念:

如果表达式 INVOKE (f, declval<ArgTypes>()..., R)(被视为未计算的操作数(子句 5))格式正确 (20.8.2),则类型为 F 的可调用f对象对于参数类型ArgTypes和返回类型 R 是可调用的。

INVOKE 位是一个虚构函数,标准使用它来定义如何调用函子和指向成员的指针。

我认为最重要的结论是以下要求:

  • 给定一个带有签名R(A...)的可调用对象,则RA...是完整的(或者可能R void),借助 INVOKE 表达式(即否则它的格式不正确,请注意使用 declval<ArgTypes>()...

我现在的论点基于调用包装器定义中的"转发到该对象的调用操作",我认为这是故意模糊的,以免过度限制。在Sig涉及一些不完整类型的std::function<Sig>的情况下,我们可以将此操作定义为"首先完成类型,然后将std::function<Sig>视为调用签名的可调用对象类型Sig"。

鉴于此,以下是我论点的关键点:

  • std::function未被描述为可调用对象或可调用的任何签名
  • 调用std::function是根据 INVOKE 完成的(20.8.11.2.4 函数调用 [func.wrap.func.inv])
  • 从可调用对象构造std::functionCallable 的,调用签名为 std::function(20.8.11.2.1 函数构造/复制/销毁 [func.wrap.func.con] 第 7 段)
  • 调用 std::functiontarget 成员是 Callable,调用签名为 std::function(20.8.11.2.5 函数目标访问 [func.wrap.func.targ])
  • std::function的所有其他操作都不用可调用对象(*)、可调用调用或以其他方式要求std::function的调用签名涉及完整类型

(*) 除非在一个构造函数的情况下,其中描述包含"如果f的目标是通过reference_wrapper或函数指针传递的可调用对象,则不得引发异常"。我认为在上下文中很明显这不会影响论点。值得一提的是,这个构造函数不参与 OP 的代码片段。

所以我想说,除非你确实使用了间接要求签名涉及完整类型的操作之一,否则你很好。

<小时 />分析标准的规定很好,

但考虑标准的意图也很重要。在这种情况下,我认为非常可取和期望std::function不需要完整的呼叫签名类型。请考虑以下事项:

// in a_fwd.hpp
struct incomplete;
using callback_type = std::function<void(incomplete)>;
callback_type make_callback();
// in b.hpp; depends on a_fwd.hpp
#include "a_fwd.hpp"
void eat_callback(callback_type);

那么在没有要求一个不相关的 TU 的情况下,我们称之为 C,即 B 的客户端可以做:

// in c.cpp
#include "b.hpp"
// somewhere in e.g. a function body
eat_callback(make_callback());

这是类型安全的,并且最大限度地减少了耦合,因为只有翻译单元 B 需要了解翻译单元 A 的详细信息。

此外,Boost.Function和libstdc++都证明了可以在没有这种要求的情况下实现std::function