动态定义函数返回类型

3

我有一个Message类,可以将其负载打包成二进制并解包。例如:

PayloadA p;
msg->Unpack(&p);

PayloadA 是一个类。

问题在于我有许多有效载荷,因此需要一个巨大的 ifswitch 语句:

if (msg->PayloadType() == kPayloadTypeA)
{
    PayloadA p;
    msg->Unpack(&p); // void Unpack(IPayload *);

    // do something with payload ...
}
else if ...

我想编写一个辅助函数来解包有效载荷。但是这个函数的类型应该是什么?类似于:

PayloadType UnpackPayload(IMessage *msg) { ... }

这里的PayloadType是一个适当负载类的typedef。我知道这几乎不可能,但我正在寻找此类解决方案。有任何想法吗?

谢谢。


函数 PayloadType() 返回什么? - David G
1
你在这里寻找多态性。 - Matthieu M.
6个回答

2
我会升级一个层次以完全避免这个问题:
#include <map>
#include <functional>

...
std::map<int, std::function<void()> _actions;
...

// In some init section
_actions[kPayloadA] = [](IMessage* msg) {
    PayloadA p;
    msg->Unpack(&p);

    // do something with payload ...
};
// repeat for all payloads

...

// decoding function
DecodeMsg(IMessage* msg) {
    _actions[id](msg);
}

为了进一步减少代码大小,尝试将Unpack变成函数模板(如果不是虚函数,可能很容易实现;如果是,可以尝试添加一层间接性,使其不再是虚函数):
class Message {
   template <class Payload>
   Payload Unpack() { ... }
};

auto p = msg->Unpack<PayloadA>();

// do something with payload ...

编辑

现在让我们看看如何避免编写长列表 _actions[kPayloadN]。这非常复杂。

首先,您需要一个助手来在静态初始化期间运行代码(即在主函数之前):

template <class T>
class Registrable
{
    struct Registrar
    {
        Registrar()
        {
            T::Init();
        }
    };

    static Registrar R;

    template <Registrar& r>
    struct Force{ };
    static Force<R> F; // Required to force all compilers to instantiate R
                       // it won't be done without this
};

template <class T>
typename Registrable<T>::Registrar Registrable<T>::R;

现在我们需要定义实际的注册逻辑:
typedef map<int, function<void()> PayloadActionsT;
inline PayloadActionsT& GetActions() // you may move this to a CPP
{
    static PayloadActionsT all;
    return all;
}

然后我们考虑解析代码:

template <class Payload>
struct BasePayload : Registrable<BasePayload>
{
    static void Init()
    {
        GetActions()[Payload::Id] = [](IMessage* msg) {
             auto p = msg->Unpack<Payload>();
             p.Action();
        }
    }
};

然后我们逐个定义所有有效载荷。
struct PayloadA : BasePayload<PayloadA>
{
    static const int Id = /* something unique */;
    void Action()
    { /* what to do with this payload */ }
}

最后,我们解析传入的消息:

void DecodeMessage(IMessage* msg)
{
    static const auto& actions = GetActions();
    actions[msg->GetPayloadType]();
}

C++有虚方法,难道不是为了穷人而设计的vtable吗? - Joker_vD
其实不完全是这样,在这里你没有一开始的类型,只有一个 int。 - J.N.
auto p = msg->Unpack<PayloadA>();。我该如何确定应该使用哪种类型作为模板参数?同样,我收到了消息,现在有 IMessage *msg;。我该如何调用 Unpackauto p = msg->Unpack<???>() - maverik
我知道的唯一可行的解决方案是使用映射(map)。但我正在寻找更加优雅的解决方案。如果我找不到其他解决方案,那么就会使用它。 - maverik
@maverik,这里已经是凌晨3点了,我应该去睡觉了。如果你真的想要更好的结果,你需要在静态初始化期间让每个有效载荷类型注册自己的解析函数到映射表中。不过,想要举出一个例子需要超过5分钟 :/ - J.N.
显示剩余3条评论

2
如何使用工厂方法根据类型创建有效负载,结合每种有效负载类型的有效负载构造器,以消息作为参数?虽然不可避免地需要使用switch语句或类似的结构,但至少这样做很直接,并且构建代码与switch语句分离。示例:
class PayloadA : public Payload
{
  public:
  PayloadA(const &Message m) {...} // unpacks from m
};

class PayloadB : public Payload
{
  public:
  PayloadB(const &Message m) {...} // as above
};

Payload * UnpackFromMessage(const Message &m)
{
  switch (m.PayloadType) :
  case TypeA : return new PayloadA(m);
  case TypeB : return new PayloadB(m);
  ... etc...
}

这个解决方案中的UnpackFromMessage()函数很容易导致内存泄漏。(并且Payload需要虚析构函数) - tp1
@tp1:内存泄漏:在哪里?虚拟析构函数:当然是一个好的实践,但并不一定需要。 - Roddy
内存泄漏会在调用UnpackFromMessage时发生,但忘记为返回的对象执行delete操作。然后需要使用虚拟析构函数。payload需要是多态类型;PoD类型对于payload不起作用。 - tp1

1
一个重要的问题是负载(payload)的差异在哪里,以及它们如何相同。在某些情况下,通过生产由负载确定类型的对象,然后通过通用于所有负载类型的虚拟接口与它们交互的系统是合理的。
另一种选择是假设您有一个有限且固定的负载类型列表,返回一个boost::variant相对容易。然后,调用一个接受变体中每种类型的函数对象的apply_visitor来处理它。
如果您只想以不同方式处理一种类型的负载,则编写“仅当类型匹配T时调用并运行lambda”的函数并不难。
因此,您可以获得以下语法:
struct State;
struct HandlePayload
{
  typedef void return_type;
  State* s;
  HandlePayload(State* s_):s(s_) {}
  void operator()( int const& payload ) const {
    // handle int here
  }
  void operator()( std::shared_ptr<bob> const& payload ) const {
    // handle bob ptrs here
  }
  template<typename T>
  void operator()( T const& payload ) const {
    // other types, maybe ignore them
  }
}

这很可爱,但你会注意到它相当间接。然而,你也会注意到可以编写带有通用类型 T 的模板代码来处理有效载荷,并针对某些情况使用诸如特征类的东西,或者使用显式特化。

如果您期望有效载荷是一种特定类型,并且只想在该情况下执行一些特殊工作,则在 boost::variant 上编写单一类型处理程序很容易。

template<typename T, typename Func>
struct Helper {
  typedef bool return_type;
  Func f;
  Helper(Func f_):f(f_) {}
  bool operator()(T const& t) {f(t); return true; }
  template<typename U>
  bool operator()(U const& u) { return false; }
};    
template<typename T, typename Variant, typename Func>
bool ApplyFunc( Variant const& v, Func f )
{
  return boost::apply_visitor( Helper<T, Func>(f), v );
}

这将在变量v上调用函数f,但仅在Variant中的类型T上调用,如果匹配成功则返回true。

使用此功能,您可以执行以下操作:

boost::variant<int, double> v = 1.0;
boost::variant<int, double> v2 = int(1);
ApplyFunc<double>( v, [&](double d) { std::cout << "Double is " << d << "\n"; } );
ApplyFunc<double>( v2, [&](double d) { std::cout << "code is not run\n"; } );
ApplyFunc<int>( v2, [&](int i) { std::cout << "code is run\n"; } );

或类似变体。


1

我看到这个问题可以通过联合体来解决。联合体的第一个成员是包含的数据包类型。

这里有一些例子:什么是联合体?


0
一个好的解决方案是使用一个共同的基类,并让所有负载从该类继承:
class PayLoadBase {
  virtual char get_ch(int i) const=0;
  virtual int num_chs() const=0;
};

然后解包看起来会像这样:

class Unpacker {
public:
   PayLoadBase &Unpack(IMessage *msg) {
      switch(msg->PayLoadType()) {
      case A: a = *msg; return a;
      case B: b = *msg; return b;
        ...
      }
   }
private:
   PayLoadA a;
   PayLoadB b;
   PayLoadC c;
};

负载已经从IPayload继承。但是IPayload具有每个负载都应该具有的最小接口。我无法做太多事情。我需要交付的类来完成这些事情。 - maverik

-1

你可以让函数返回一个 void *。Void指针可以转换为任何其他类型。


2
“void *” 是我能想到的最糟糕的情况。无论如何,如果函数返回“void”,我需要再次使用巨大的“if”。 - maverik

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