【问题标题】:C++ - is it possible to extract class and argument types from a member function type in a template?C++ - 是否可以从模板中的成员函数类型中提取类和参数类型?
【发布时间】:2013-01-24 21:16:15
【问题描述】:

我想用模板类包装符合类型“void (ClassType::Function)(ArgType)”的成员函数。稍后,我想将 ClassType 的实例传递给此模板的实例并让它调用包装的方法:

class Foo {
 public:
  Foo() : f_(0.0) {}
  void set(double v) { f_ = v * 2.1; }
  double get() { return f_; }
 private:
  double f_;
};

template <typename ArgType, typename ClassType, void (ClassType::*Method)(ArgType)>
class Wrapper {
 public:
  explicit Wrapper(ClassType *cls) : cls_(cls) {}

  void do_something(ArgType value) {
    (cls_->*Method)(value);
  }

 private:
  ClassType *cls_;
};

#include <iostream>
int main(int argc, char ** argv) {
  Foo foo;
  Wrapper<double, Foo, &Foo::set> wrapper(&foo);

  wrapper.do_something(1.0);
  std::cout << foo.get() << std::endl;
  // outputs "2.1"
  return 0;
}

注意在 Wrapper 的实例化中“Foo”被指定了两次——这里看起来是多余的。

所以我想知道的是是否可以避免模板参数ClassType。例如,如果可以从成员函数指针参数中隐含或提取它,则不需要在 Wrapper 的实例化中显式指定它。

以类似的方式,避免显式指定 ArgType 也很有用,因为(也许)它可以从 Foo::set 中确定?

这在 C++ 中可行吗?也许是这些(完全幻想的)路线:

template <void (ClassType::*Method)(ArgType)>
class Wrapper2 {
 public:
  explicit Wrapper(Method::ClassType *cls) : cls_(cls) {}

  void do_something(Method::ArgType value) {
    (cls_->*Method)(value);
  }

 private:
  Method::ClassType *cls_;
};

// ...

int main() {
  Foo foo;
  Wrapper<&Foo::set> wrapper(&foo);
  // ...
}

或者,也许可以调用另一个级别的模板魔术来执行以下操作:

Wrapper<Magic<&Foo::set> > wrapper(&foo);

我很想知道有哪些机制可用(如果有的话)。

我要求使用 C++03,而不是 C++11,但我也想知道 C++11 可能提供什么。

编辑:更多信息 - 我打算使用这种机制来包装约 300 个成员函数(全部属于 ClassType,或一组非常相似的类),但只需要考虑大约六个左右的签名:

  • void (ClassType::Function)(ArgType) - 其中 ArgType 是“浮动”
  • void (ClassType::Function)(ArgType) - 其中 ArgType 是“整数”
  • void (ClassType::Function)(bool)
  • void (ClassType::Function)(IndexType, ArgType) - 以上三个带有额外的“索引”参数

例如,成员函数是我在大型配置“集合”类中称为“属性”的“设置器”函数(而不是上面的简单 Foo):

class MyPropertyCollection {
 public:
  void set_oink(double value) { oink_ = value; }
  void set_bar(int value) { bar_ = value; }
  void set_squee(bool value) { squee_ = value; }
 private:
  double oink_;
  int bar_;
  bool squee_;
};

// elsewhere
WrapperCollection wrapper_collection;  // a simple set of wrapper objects, accessed by id
MyPropertyCollection property_collection;
wrapper_collection.add(PROPERTY_OINK_ID, new Wrapper<double, MyPropertySet, &MyPropertySet::set_oink>(&property_collection);
wrapper_collection.add(PROPERTY_BAR_ID, new Wrapper<int, MyPropertySet, &MyPropertySet::set_bar>(&property_collection);
wrapper_collection.add(PROPERTY_SQUEE_ID, new Wrapper<bool, MyPropertySet, &MyPropertySet::set_squee>(&property_collection);
// +300 more

【问题讨论】:

  • 我很确定使用类专业化是可能的。因为推导是反向的,你可以将方法声明类型分解为 3 种模板类型,从只有一种的基本模板声明。噗,提取。我马上在 gcc 中尝试一下。

标签: c++ templates template-meta-programming member-function-pointers


【解决方案1】:

这是对 C++11 结构 ::std::mem_fn + ::std::bind 的糟糕重新实现。以下是您可以如何使用这些:

#include <functional>

int main() {
   Foo foo;
   auto wrapper = ::std::bind(::std::mem_fn(&Foo::set), ::std::ref(foo), _1);
   wrapper(5); // Calls foo.set(5)
}

但是,当然,您需要 C++03 解决方案。使用 Boost 可以在 C++03 中实现这一点。我也相信在带有 TR1 的 C++03 中可以做到这样的事情。您可以通过查看 #include &lt;tr1/functional&gt; 是否有效来判断您是否拥有它。如果您有 TR1,它们会显示在 ::std::tr1 命名空间中。

现在,有一种方法不是。您已将函数指针本身作为类的类型签名的一部分。这是一件很奇怪的事情,但正如你已经知道的那样,这肯定是可能的。能够在编译时确定 ClassTypeArgType 值是很棘手的。您可以使用模板函数参数匹配来做到这一点,但不是很有用,因为 C++03 没有 auto

【讨论】:

  • 谢谢,我会看看 TR1 和/或 Boost 的变体。我将函数指针作为类型签名的一部分的原因是因为我想扩展它以匹配其他类型的函数,并自动进行正确的特化匹配。但是现在我考虑一下,我想知道这是否可以通过函数类型上的参数匹配来实现 - 但我仍然需要将 ClassType 和 ArgType 指定为模板参数,以便可以正确指定参数类型。
  • @meowsqueak:我考虑了一下,只要你有指针值作为模板参数,你无法通过函数参数匹配以任何方式解决你的问题。问题是模板参数必须是一个常量表达式。并且函数参数不是常量表达式。因此,虽然您可以使用模板函数参数推导从随机指针值中提取参数和类类型,但您不能将该指针值用作返回类型的模板参数之一。如果这有任何意义的话。
  • 我认为这是有道理的。我现在改变了我的方法——我没有使用成员函数指针作为模板参数,而是将它作为普通参数传递。然后我有一小组模板来处理我需要包装的每个签名。这意味着包装函数的签名(包括名称)不再是类型的一部分。到目前为止,它运行良好。
【解决方案2】:

阅读你所做的让我想到了几个选择:

1) 将实例化封装在继承中。这会将可怕的东西移到您的定义中。

class FooWrapper : public Wrapper< double, Foo, &Foo::set >, public Foo
{
public:
    FooWrapper() : Wrapper(this){}
};

您的逻辑代码如下所示:

  FooWrapper fooWrapper;
  fooWrapper.do_something(1.0);
  std::cout << fooWrapper.get() << std::endl;

这意味着您没有消除双模板参数,您只是移动了它们。

2) 有一种更通用的方式来包装它,在一个级别:

  template<typename argType1, class classType1>
class FooWrapper2 : public Wrapper<argType1, classType1, &classType1::set>, public classType1
{
public:
    FooWrapper2()
        : classType1(),
          Wrapper<argType1, classType1, &classType1::set>(this)
    {

    }
};

这种方式的缺点是看起来逻辑更复杂,但你不必每次都定义一个新的包装器,只需为每个签名定义一个新的包装器:

  FooWrapper2<double, Foo> fooWrapper2;
  fooWrapper2.do_something(1.0);
  std::cout << fooWrapper2.get() << std::endl;

3) 保持与模板思想一致,可以包装包装器:

  template<typename argType1>
class FooWrapper3 : public FooWrapper2<argType1, Foo>
{
public:
    FooWrapper3()
    {

    }
};

由此产生的逻辑代码看起来更好一些,但您不得不为要包装的每种类型重新子类化(使用特定代码,而不仅仅是使用模板):

  FooWrapper3<double> fooWrapper3;
  fooWrapper3.do_something(1.0);
  std::cout << fooWrapper3.get() << std::endl;

4) 这个选项废弃了基础包装类并使用了一个接口。只需像传递包装器一样传递接口,就可以执行大多数操作。

template <typename ArgType>  
class Do_something {  
 public:  

  virtual void do_something(ArgType value) = 0;  

};  

template<typename ArgType>  
class FooWrapper4 : public Foo, public Do_something<ArgType>  
{  
public:  
    virtual void do_something(ArgType value)  
    {  
        set(1.0);  
    }  
};  

我玩过的测试程序:

class Foo {
 public:
  Foo() : f_(0.0) {}
  void set(double v) { f_ = v * 2.1; }
  double get() { return f_; }
 private:
  double f_;
};


template <typename ArgType, typename ClassType, void (ClassType::*Method)(ArgType)>
class Wrapper {
 public:
  explicit Wrapper(ClassType *cls) : cls_(cls) {}

  void do_something(ArgType value) {
    (cls_->*Method)(value);
  }

 private:
  ClassType *cls_;
};


class FooWrapper : public Wrapper< double, Foo, &Foo::set >, public Foo
{
public:
    FooWrapper() : Wrapper(this){}
};


template<typename argType1, class classType1>
class FooWrapper2 : public Wrapper<argType1, classType1, &classType1::set>, public classType1
{
public:
    FooWrapper2()
        : classType1(),
          Wrapper<argType1, classType1, &classType1::set>(this)
    {

    }
};

template<typename argType1>
class FooWrapper3 : public FooWrapper2<argType1, Foo>
{
public:
    FooWrapper3()
    {

    }
};

template <typename ArgType>
class Do_something {
 public:

  virtual void do_something(ArgType value) = 0;

};

template<typename ArgType>
class FooWrapper4 : public Foo, public Do_something<ArgType>
{
public:
    virtual void do_something(ArgType value)
    {
        set(1.0);
    }
};

#include <iostream>
int main(int argc, char ** argv) {
  Foo foo;
  Wrapper<double, Foo, &Foo::set> wrapper(&foo);

  wrapper.do_something(1.0);
  std::cout << foo.get() << std::endl;

  FooWrapper fooWrapper;
  fooWrapper.do_something(1.0);
  std::cout << fooWrapper.get() << std::endl;
  // outputs "2.1"

  FooWrapper2<double, Foo> fooWrapper2;
  fooWrapper2.do_something(1.0);
  std::cout << fooWrapper2.get() << std::endl;

  FooWrapper3<double> fooWrapper3;
  fooWrapper3.do_something(1.0);
  std::cout << fooWrapper3.get() << std::endl;

  FooWrapper4<double> fooWrapper4;
  fooWrapper4.do_something(1.0);
  std::cout << fooWrapper4.get() << std::endl;

  return 0;
}

【讨论】:

  • 亲爱的未来的人们,我似乎无法让格式在其中一个代码段上正常工作,我深表歉意。
  • 感谢您的回复。关于第 1 点,我需要扩展它以包装超过 300 个成员函数(来自第三方的配置接口的一部分),因为我知道只有大约六个不同的方法签名。为每个类封装继承将涉及编写 300 个单独的类,除非我将它们模板化(这最终对我没有任何好处)。
  • @meowsqueak,既然是这种情况,方法 2 应该可以为您提供所需的内容。您应该只需要定义其中的 6 个。
  • 根据我所说的,方法 2 看起来很合适,但是我误解了我所说的“签名”的含义——我现在意识到方法名称是签名的一部分,而且它也发生了变化( set_foo、set_bar 等)。所以我实际上有 300 个独特的签名,这很不幸。我可能不得不先重新考虑那个界面。
  • 如果函数名称开始改变,我不知道有什么好的模板方法来做到这一点。如果你有一个所有函数调用的列表,你可以编写一个正则表达式来为你生成代码,但这不像模板的东西那么有趣。
【解决方案3】:

在 C++11 中,您可能会使用 lambda,例如:

template <typename X, typename ARG>
std::function<void(X*, ARG)> wrapper(void (X::*mfp)(ARG))
{
    return [=](X *x, ARG arg) {
       (x->*mfp)(arg);
    };
}

使用 VisualC++(至少与 VS2013 一样新),在捕获成员函数指针(或遇到崩溃)时使用按值捕获 [=]

游乐场:

#include <iostream>
#include <functional>

struct A {
    virtual void a(int i) { std::cout << "A: " << i << std::endl; }
};

struct B {
    virtual void b(int i) { std::cout << "B: " << i << std::endl; }
};

template <typename X, typename ARG>
std::function<void(X*, ARG)> wrapper(void (X::*mfp)(ARG)) {
    return [=](X *x, ARG arg) { (x->*mfp)(arg); };
}

int main()
{
    auto g = wrapper(&B::b);
    B b;
    g(&b, 3);
    auto h = wrapper(&A::a);
    A a;
    h(&a, 4);
    return 0;
}

【讨论】:

    【解决方案4】:
    struct MyClass
    {
        MyClass& Move(MyClass& m) { return *this; }
    };
    
    typedef MyClass& (MyClass::*MethodT) (MyClass&);
    
    template< typename T >
    struct ExtractType : std::false_type
    {
    };
    
    template< typename R, typename C, typename A >
    struct ExtractType< R (C::*)(A) >
    {
        typedef C type;
    };
    
    static_assert( std::is_same< ExtractType< MethodT >::type, MyClass >::value, "oops" );
    

    它似乎适用于我的 gcc 4.8 版本。
    它就像我在评论中提到的那样工作,它是编译器在专业化检查期间执行的“反向模式匹配”。这是非常强大的。
    所以你看,我们指定了某种模式,如果 T 类型尊重,编译器将把它分解为组成它的三个子类型:RCA。分别是返回类型、类类型和参数。

    但是您可以看到它适用于一个参数。当我们有未定义数量的参数时怎么办?
    也许是检查器类的列表,或者使用可变参数模板?

    坦率地说,我什至不确定这是否适用于void。我认为 void 总是不可能放置在模板中,因此它将导致这个ExtractType 类的许多版本支持所有可能的声明组合。或者在我看来是这样。

    编辑:

    好的,所以我完全随机地放弃它,但在 C++11 中它似乎比我预期的要好得多,这在 gcc 4.8 上是可以的:

    struct MyClass
    {
    };
    
    typedef int (MyClass::*MethodT) (bool);
    typedef void (MyClass::*VV) ();
    typedef void (MyClass::*IL) (int, long);
    
    template< typename T >
    struct ExtractType : std::false_type
    {
    };
    
    template< typename R, typename C, class...A >
    struct ExtractType< R (C::*)(A...) >
    {
        typedef C type;
        typedef R returntype;
    };
    
    static_assert( std::is_same< ExtractType< MethodT >::type, MyClass >::value, "oops" );
    static_assert( std::is_same< ExtractType< VV >::type, MyClass >::value, "oops" );
    static_assert( std::is_same< ExtractType< IL >::type, MyClass >::value, "oops" );
    
    static_assert( std::is_same< ExtractType< MethodT >::returntype, int >::value, "oops" );
    static_assert( std::is_same< ExtractType< VV >::returntype, void >::value, "oops" );
    static_assert( std::is_same< ExtractType< IL >::returntype, void >::value, "oops" );
    

    疯狂的部分是它不介意返回类型中的void。当然,它是 C++11。

    【讨论】:

      猜你喜欢
      • 2020-12-16
      • 1970-01-01
      • 1970-01-01
      • 2011-07-06
      • 1970-01-01
      • 2012-10-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多