【问题标题】:Template argument inference by constructor arguments通过构造函数参数推断模板参数
【发布时间】:2013-01-09 15:04:50
【问题描述】:

可以使用以下方法“剥离”函数参数类型:

void foo_double(double a)
{
}

void foo_int(int a)
{

}

template <class R, class A0>
void bar(R (*fp)(A0))
{   
    // Do something related with A0
}

int main()
{
    bar(foo_double); // A0 is double
    bar(foo_int);    // A0 is int
}    

是否可以对类构造函数进行相同的“参数类型剥离”?

编辑:

我相信我在原始代码sn-p中没有清楚地解释自己。这是完整的场景。

我有多个类C1,...,Cn,我需要将它们作为函数公开给python。让我们假设所有类都有一个共同的void Run() 方法。但是,这些类的构造函数接受不同的参数。为了向 python 公开函数,我使用 boost.python 自动将函数导出到适当的 python 函数,同时处理所有类型转换(主要是原语)。

我的第一个解决方案是:

class C1 
{
public:
    C1() {}
    void Run();
};

class C2
{
public:
    C2(double a) {}
    void Run();
};

template <class T>
void python_wrapper()
{
    T instance();
    instance.Run();
}

template <class T, class A0>
void python_wrapper(A0 a0)
{
    T instance(a0);
    instance.Run();
}

BOOST_PYTHON_MODULE(_pythonmodule)
{
    // This is boost.python stuff
    python::def("f1", python_wrapper<C1>);
    python::def("f2", python_wrapper<C2, double>);
}

而且...它有效。

我现在想要完成的是在推断构造函数参数类型时使用python_wrapper&lt;C2&gt; 而不是python_wrapper&lt;C2, double&gt;

正如我在原帖中所展示的那样。如果我包装函数而不是类,我可以完成类似的事情。

【问题讨论】:

  • 没有。你为什么要那个?知道函数参数类型大多是无害的,呃,我的意思是,大多是无用的。 99% 的时间你想知道函数是否可以以某种方式调用,而不是参数是否具有特定类型。
  • 构造函数没有名字,不能像普通成员函数那样被调用。我认为std::is_constructible 对于您要解决的任何问题都可能是一个有用的特征。
  • @R.MartinhoFernandes,我想生成一个函数bar(要导出到python),它通过调用构造函数来创建一个类的实例。没有明确指定 c'tor 参数。我会使用一组封闭的类。我不想为此修改其中任何一个。
  • @KerrekSB,谢谢。我考虑过使用它,但无法提出解决方案。
  • 如何从python调用这个bar函数? bar(MyClass,arg1,arg2)?

标签: c++ templates boost-python template-meta-programming


【解决方案1】:

无法推断类型的构造函数的参数。

C++98/03 和 C++11 规范明确列出了可能发生类型推导的上下文(请参阅第 14.8.2 节及其小节)。扣除是模板编程的生活增强资格,而不是强制性的。任何可以通过演绎完成的事情,也可以通过显式调用来实现。因此,为了使推论成为可能,需要明确地为函数模板提供构造函数。

但是,这是不可能的。如 C++98/03 和 C++11 规范的第 12.1 节所述,构造函数没有名称。此外,C++98/03 的第 12.1.12 节和 C++11 的第 12.1.10 节规定不得采用构造函数的地址。因此,无法提供标识符;因此,不能进行扣除。


由于不可能进行扣除,因此可能值得考虑替代解决方案。每个解决方案都有自己的优缺点,但所有这些都需要在构造函数之外的某些上下文中明确列出参数类型:

  • 将构造函数的参数类型提供给函数模板。
    • 优点:
      • 相当简单。
    • 缺点:
      • 类型和类型的构造函数的参数类型之间的关联不是很明显。
      • 关联不可重复使用。例如,如果将关联传递给多个函数模板或类模板,则需要复制关联。
      • 对于每个具有 arity 为 1 或更大的构造函数的类型都是必需的。
  • 保持与类型特征的关联。
    • 优点:
      • 可重复使用。
      • 类型和类型的构造函数的参数类型之间的关联更加明显。
      • 不过分复杂。
    • 缺点:
      • 编码比直接提供与函数模板的关联稍微复杂一些。
      • 对于每个具有 arity 为 1 或更大的构造函数的类型都是必需的。
  • 为每种类型创建一个工厂函数。
    • 优点:
      • 非常简单。
      • 可重复使用。
      • 类型和类型的构造函数的参数类型之间的关联非常明显。
    • 缺点:
      • 每个类型都需要,即使 arity 为 0。这可以通过侵入性工厂函数来缓解,因为不会因范围而产生歧义。对于非侵入式工厂函数,签名可能会发生冲突,因此函数名称必须是唯一的。
  • 大量使用元编程来获得构造函数参数类型的向量。然后模板代码将遍历不断增长的列表,试图识别一个可行的匹配。
    • 优点:
      • 如果类型具有相似的构造函数,则向量中的单个条目可以作为多种类型的可行匹配。
    • 缺点:
      • 要复杂得多。
      • 可能需要修改编译器参数以支持模板深度。

鉴于您的环境所描述的情况:

  • 有很多类。
  • 有些已经存在,不应更改。
  • 每天都会写更多这些内容。
  • 构造函数是独一无二的。

当考虑到 C++ 规范时,我相信我们已经定义了 Kobayashi Maru。您必须权衡利弊,以确定哪种方法可以适应您的环境。最简单的方法可能已经是您现有的方法了,因为它只需要一个位置来更改代码,因为创建更多类型。


尽管如此,这是一种使用类型特征的方法,它以非侵入性方式提供有关类型构造函数的信息。如果没有 C++11 功能,例如可变参数模板,就会有一些样板代码。此外,实现可能不会涵盖所有情况,例如多个构造函数。

使用原始问题中提供的类:

class C1 
{
public:
  C1();
  void Run();
};

class C2
{
public:
  C2(double a);
  void Run();
};

将使用代表构造函数特征的模板。我使用Boost.MPL 提供的类型列表来表示构造函数的参数类型。默认的constructor_traits 表示不需要任何参数。

/// @brief constructor_traits is a type_trait that is used to noninvasively
///        associated T with constructor meta information, such as T'
///        constructor's argument types.
///
///        For example, if Foo has a constructor that accepts a single
///        integer, then constructor_traits<Foo>::type should be
///        boost::mpl::vector<int>.
template <typename T>
struct constructor_traits
{
  typedef boost::mpl::vector<> type;
};

然后,此特征专门用于具有接受参数的构造函数的类型,例如 C2

/// Specialize constructor_traits for C2 to indicate that its constructor
/// accepts a double.
template <>
struct constructor_traits<C2>
{
  typedef boost::mpl::vector<double> type;
};

boost::mpl::vector 是代表构造函数参数的类型列表。它通过boost::mpl::at 提供随机访问。为了提供对元素的更简洁的访问,引入了一个辅助类型:

/// @brief Helper type that makes getting the constructor argument slightly
///        easier.
template <typename Vector,
          std::size_t Index>
struct get_constructor_arg
  : boost::mpl::at<Vector, boost::mpl::int_<Index> >
{};

将函数公开给 Boost.Python 时,所需的语法是只提供一种类型。函数模板或类模板都可以用来解决这个问题。我决定使用类模板,因为它减少了一些样板代码。

/// @brief runner type is used to provide a static run function that
///        will delegate the construction and running of type T based
///        on T's constructor_traits.
template <typename T,
          typename Args = typename constructor_traits<T>::type,
          std::size_t = boost::mpl::size<Args>::value>
struct runner
{
  static void run()
  {
    T().Run();
  }
};

然后,此模板专门针对构造函数接受的参数数量。下面专门接受一个论点。这是由特化的模板参数列表中的1 确定的。

/// Specialization for runner for types with have a single argument
/// constructor.
template <typename T,
          typename Args>
struct runner<T, Args, 1>
{
  static void run(typename get_constructor_arg<Args, 0>::type a0)
  {
    T(a0).Run();
  }
};

函数模板也可以用来解决这个问题。我决定使用类模板是因为:

  • 不需要 SFINAE。需要使用enable_if 构造来选择正确的模板。
  • 能够为类模板提供默认模板参数可以避免多次获取constructor_trait

生成的 Boost.Python 调用如下所示:

BOOST_PYTHON_MODULE(_pythonmodule)
{
  boost::python::def("f1", &runner<C1>::run);
  boost::python::def("f2", &runner<C2>::run);
}

完整代码如下:

#include <iostream>
#include <boost/mpl/vector.hpp>
#include <boost/python.hpp>

class C1 
{
public:
  C1() {}
  void Run() { std::cout << "run c1" << std::endl; }
};

class C2
{
public:
  C2(double a) : a_(a) {}
  void Run() { std::cout << "run c2: " << a_ << std::endl;}
private:
  double a_;
};

/// @brief constructor_traits is a type_trait that is used to noninvasively
///        associated T with constructor meta information, such as T'
///        constructor's argument types.
///
///        For example, if Foo has a constructor that accepts a single
///        integer, then constructor_traits<Foo>::type should be
///        boost::mpl::vector<int>.
template <typename T>
struct constructor_traits
{
  typedef boost::mpl::vector<> type;
};

/// Specialize constructor_traits for C2 to indicate that its constructor
/// accepts a double.
template <>
struct constructor_traits<C2>
{
  typedef boost::mpl::vector<double> type;
};

/// @brief Helper type that makes getting the constructor argument slightly
///        easier.
template <typename Vector,
          std::size_t Index>
struct get_constructor_arg
  : boost::mpl::at<Vector, boost::mpl::int_<Index> >
{};

/// @brief runner type is used to provide a static run function that
///        will delegate the construction and running of type T based
///        on T's constructor_traits.
template <typename T,
          typename Args = typename constructor_traits<T>::type,
          std::size_t = boost::mpl::size<Args>::value>
struct runner
{
  static void run()
  {
    T().Run();
  }
};

/// Specialization for runner for types with have a single argument
/// constructor.
template <typename T,
          typename Args>
struct runner<T, Args, 1>
{
  static void run(typename get_constructor_arg<Args, 0>::type a0)
  {
    T(a0).Run();
  }
};

BOOST_PYTHON_MODULE(example)
{
  boost::python::def("f1", &runner<C1>::run);
  boost::python::def("f2", &runner<C2>::run);
}

以及测试输出:

>>> import example
>>> example.f1()
run c1
>>> example.f2(3.14)
run c2: 3.14

【讨论】:

  • 谢谢。但是我必须为我想要包装的每个课程专门constructor_traits(我有很多)。它与我的解决方案有何不同?您仍然必须指定 c'tor 参数类型,仅在其他地方。
  • @Albert:它将信息与其用法解耦,类型与构造函数的关联可能比&lt;C2, double&gt; 更明显。正如一些海报所说,由于构造函数的地址和类型不可用,因此可能没有一种技术可以让您精确推断。你的许多类有类似的构造函数吗?维护一个构造函数类型信息列表,并让模板迭代尝试每一个是一个可行的解决方案吗?还是大多数构造函数都是独一无二的?
  • 目前它们是独一无二的。但是其中更多的内容是每天编写的(甚至是临时/测试课程)。我几乎无法控制那些 c'tors。正如我怀疑的那样,答案是“不,你不能”。这让我感到惊讶,因为它可以很容易地完成功能。我从你的回答中学到了,但目前我想我会坚持我的解决方案。这对用户来说会更直接。
  • 你好。你能指出一些关于“大量使用元编程来拥有构造函数参数类型的向量”的文章吗?我目前正在尝试实现此功能。我正在尝试为给定的typename Constructible获取编译时tuple
【解决方案2】:

不能取构造函数的地址(C++98 Standard 12.1/12 Constructors - “12.1-12 Constructors - “不得取构造函数的地址。”)

如果所有类共享一个基类,那么您可以创建一个工厂,但如果没有 C++11 可变参数模板,我不知道如何进行参数转发。

template <typename type>
static type*  construct(){
    return new type();
}


auto value = &construct<int>;

你现在有一个可绑定的函数,当被调用时,它会产生一个类型*

auto value = &construct<int>;
int* newInt = (*value)();
delete newInt;

为了允许不同的构造函数输入,这里使用 C++11

template <typename type>
class constructor {

public:
    template<typename ...Args>
    static std::shared_ptr<type>  alloc(Args&& ...args){
        return std::shared_ptr<type>(new type(std::forward<Args>(args)...));
    }
    // placement new
    template<typename ...Args>
    static std::shared_ptr<type>  realloc( std::shared_ptr<type>& object,Args&& ...args){
        (new (&(*object)) type(std::forward<Args>(args)...));
        return object;
     }
};

class fooBar{
public:
    fooBar(int x,float f){
    }
};
typedef std::shared_ptr<fooBar> fooBarPtr;

用法:

fooBarPtr a = constructor<fooBar>::alloc(5,0.0f);

或者这可能更接近操作人员想要的。

class fooy{
    int _x = 0;
public:
    fooy(int argCount, va_list& list){
        if( argCount > 0){
            _x = va_arg(list, int);
        }
    }
};

template <typename type>
std::shared_ptr<type> alloc(int argCount,...) {
    va_list list;
    va_start(list, argCount);
    auto result = std::shared_ptr<type>( new type(argCount,list) );
    va_end(list);
    return result;
}

用法:

auto foo1 = alloc<fooy>(0); // passing zero args
auto foo2 = alloc<fooy>(1,1234); // passing one arg

【讨论】:

  • 一点也不差,但它不能解释任何事情......这应该如何工作?
  • 这是否没有提供足够的信息来回答:“是否可以对类构造函数执行相同的“参数类型剥离”?”我还应该扩展什么?
  • 如果您扩展它以允许通过放置新而不是动态分配来放置,我很乐意为您的答案 +1!
  • @Nice:我发表评论时并没有,它只是第一批代码而已
  • @Agentlien 我认为这就是您想要的。我没时间去测试了~!
【解决方案3】:

由于您不想更改现有类,因此无法自动在 C++ 中对这些类进行推断。通过从源代码中提取签名信息,您可以在 C++ 之外为这些类执行此操作。这种提取可以手动完成,也可以通过脚本甚至 C++ 程序自动完成。

对于每个现有的类 E,使用提取的信息来定义一个类模板的特化,该模板定义了一个静态成员函数 func,该函数将其参数(如果有)传递给类 E 的构造函数并调用 @ 987654322@方法。

对于新类,只需要求它们定义func,并让类模板默认使用现有的func

然后,一般的 Python 包装器定义代码可以推断 func 的参数,您已经展示了您知道如何做。

这意味着定义类的人只需要开始为新的此类添加func


在稍高一点的层面上,我认为值得花一些时间让相关人员参与进来,并通过定义无数类来了解如何采用和巩固一个不好的设计。目的是防止这种情况在未来再次发生。我认为如何处理这将取决于它是如何发生的。

【讨论】:

    【解决方案4】:

    我不确定这将如何与 python::def 交互,但总的来说,这就是可变参数模板的用途:

    struct C1{
      C1(){}
      void run(){}
    };
    
    struct C2{
      C2(int){}
      void run(){}
    };
    
    template<typename CLASS, typename... PARAMS>
    void call(PARAMS... params) {
      CLASS inst(params...);
      inst.run();
    }
    
    main() {
      call<C1>();
      call<C2>(42);
    }
    

    它是 c++11,但从 4.3 版开始在 gcc 中受支持

    【讨论】:

    • 谢谢。两个问题。我使用VS2012。我需要将函数接口暴露给 python - 不要调用它。
    • 我没有使用过 VS2012,但I've read 如果您调整设置,它具有可变模板支持。我不知道 python::def 实际上是什么。您可能需要在那里明确地专门调用(但至少您不需要多次编写调用)
    • @dspeyer:我相信 Albert 想要一个不需要在构造函数本身之外显式提供构造函数参数类型的解决方案。例如,&amp;call&lt;C1&gt; 的类型为 void(*)()&amp;call&lt;C2&gt; 的类型为 void(*)(int)call 函数模板在实例化期间仅根据提供的类推断参数类型,而不是调用。跨度>
    猜你喜欢
    • 2021-08-13
    • 1970-01-01
    • 1970-01-01
    • 2020-10-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-10-21
    • 1970-01-01
    相关资源
    最近更新 更多