【问题标题】:C++ how to replace constructor switch?C++如何替换构造函数开关?
【发布时间】:2019-03-05 14:26:12
【问题描述】:

我想用更优雅的东西代替大开关。

class Base
{
public:
  Base(void*data, int size);
  virtual void Something() = 0;
}
class A : public Base
{
public:
  A(void*data, int size) : Base(data, size) {}
  void Something() override;
}
class B : public Base
{
public:
  B(void*data, int size) : Base(data, size) {}
  void Something() override;
}
...

{
  char c = input;
  switch (c)
  {
    case 'a':
    {
      A obj(data, size);
      obj.Something();
      break;
    }
    case 'b':
    {
      B obj(data, size);
      obj.Something();
      break;
    }
    ...
  }
}

正如您在示例类中看到的那样,AB 与外部没有区别。 我想找到一种方法来消除仅实例化正确类并在其上调用相同方法的开关,在我的真实代码中,该开关超过 1000 行长,并且有更多的类,但我找不到任何方法摆脱它。

在实际代码中,我有 2 个枚举而不是 char,还有更多开关,但我希望问题很清楚。

我的一个想法是在基类上使用模板,但我没有找到一种方法来实例化正确的子类而不需要那个巨大的开关。

编辑 我从网络接收数据包并想解析它并处理它。这些类a, b, ... 没有任何私有或公共成员,基类只有指向日期的原始指针和指向响应套接字的共享指针(也来自构造函数)。

我希望编译器使用 templates? 或其他一些机制为我生成该开关,以删除重复的源代码。目前它仍处于测试阶段,但它每秒处理大约 1000 个数据包,所以我不想在堆上分配和释放时移除开关并失去性能。

【问题讨论】:

  • 或许看看工厂设计模式?
  • 你忘记了 break 之间的情况
  • 是的,我删除了中断,因为它说我的代码太多而其他文本太少:(

标签: c++ c++17


【解决方案1】:

找到static_for 的实现,它本身就很简单:

using list = std::tuple<A, B, C, D, E, F, G, ...>;

const auto n = c - 'a';
static_for<std::tuple_size<list>()>([&](auto N){
    if (n != N)
        return;
    using T = std::tuple_element_t<list, N>;
    T obj(data, size);
    obj.Something();
});

进一步考虑:

  1. 如果它们都具有相同的多态接口,您可以决定只使用它来创建对象。

  2. 如果你的射程中有空位,if constexprstd::is_same 是你的朋友。

  3. 使用一些专用的类型列表类型而不是 std::tuple 可能会更好,但这在紧要关头可以工作。

static_for() 的未完善、快速和肮脏的示例实现:

template <std::size_t Is, class F>
void static_for_impl(F&& f, std::index_sequence<Is...>) {
    f(std::integral_constant<std::size_t, Is>()), ...;
}

template <std::size_t N, class F>
void static_for(F&& f) {
    static_for_impl(f, std::make_index_sequence<N>());
}

【讨论】:

【解决方案2】:

如果构造函数完全相同并且Something 方法的调用方式相似,那么您应该可以使用这样的模板:

template<typename T>
void DoSomething(void*data, int size){
    T t(data, size);
    t.Something();
}
..
{
    switch(input){
        case 'a': DoSomething<A>(..); break;
        case 'b': DoSomething<B>(..); break;
    }
}

如果您想验证模板是Base 的派生类,您可以使用is_base_of

由于您打开了一个未知变量(在本例中为 char),我不确定您将如何最小化开关,除非遵循模式 suggested by Alan Birtles

【讨论】:

  • 我的想法类似于DoSomething&lt;input&gt;(...),其中编译器会在后台生成该开关,a -&gt; A 可能太明显了,但在实际代码中有更长的名称,我想防止例如,添加案例时的人为错误。
  • 但它怎么可能断定字符'a'应该以某种方式创建一个A类?两者之间没有逻辑联系。
  • 我的意思是我有与类名匹配的枚举常量,但许多类的名称相似,只是略有不同
  • @Jakub 你能举例说明你的意思吗? (您可以edit您的问题添加更多信息)
【解决方案3】:

这样的事情应该可以工作:

#include <map>
#include <functional>
#include <memory>

typedef std::function< std::unique_ptr< Base >( void* data, int size ) > Factory;
std::map< char, Factory > factories =
{
    { 'a', []( void* data, int size ){ return std::make_unique<A>( data, size ); } },
    { 'b', []( void* data, int size ){ return std::make_unique<B>( data, size ); } }
};
char input = 'a';
void* data = 0;
int size = 0;
auto factory = factories.find( input );
if ( factory != factories.end() )
{
    factory->second( data, size )->Something();
}

您只需在每个类的工厂列表中添加一行。

如果您使用的是从 0 开始的连续值的枚举,那么您可以只使用数组而不是 std::map,例如:

enum class Class
{
    a,
    b
};

Factory factories[] =
{
    []( void* data, int size ){ return std::make_unique<A>( data, size ); },
    []( void* data, int size ){ return std::make_unique<B>( data, size ); }
};
Class input = Class::a;
factories[static_cast<size_t>(input)]( data, size )->Something();

【讨论】:

  • 这个用例的代码是在堆上还是在栈上分配内存?我不想在堆上分配内存,因为这将是大约 1000 次 28 B 的分配和释放(这些类的实际大小)
  • 它将在堆上分配,如果你想避免堆分配,那么你当前的解决方案可能是唯一的选择,否则你最终会得到对象切片。您实际上是根据输入调用每个类还是仅调用一个类?你也可以让工厂返回单例,这样每个类只在合适的时候创建一次
  • 只调用了一个类,基类提供了一些方法,不会在每个子类中重复它们,例如通过边界检查从数据包中读取特定类型的数据。子类调用这些通用方法来解析数据包,并针对该数据包做一些特定的事情。
  • 如果只调用 1 个类,则只有 1 个分配。您可以使工厂映射本身静态,以便它只创建一次。如果您的枚举包含从0n 的连续值,那么您可以只使用静态数组而不是map
  • 单例实例不起作用,因为它所处理的数据是在构造函数中传递的,并对它们进行了基本检查,我正在考虑将其全部设为静态,并将数据传递给方法,这意味着我会需要在每个方法中进行检查,但 switch 会更短。但我仍然想知道是否有一些优雅的解决方案可以让我获得正确的类而无需手动编写该开关。
【解决方案4】:

您可以使用简单的工厂方法通过所需的类型和构造函数参数创建对象,如下例所示。使用继承和虚函数时不要忘记虚析构函数。

#include <memory>

class Base
{
public:
    Base(void* data, int size) {};
    virtual ~Base() {}
    virtual void Something() = 0;
};

class A : public Base
{
public:
    A(void* data, int size) : Base(data, size) {}
    void Something() override {};
};

class B : public Base
{
public:
    B(void* data, int size) : Base(data, size) {}
    void Something() override {};
};

Base* MyFactory(char type, void* data, int size)
{
    switch (type)
    {
        case 'a': return new A(data, size);
        case 'b': return new B(data, size);
        default:
            return nullptr;
    }
}

int main()
{
    std::unique_ptr<Base> obj1(MyFactory('a', nullptr, 1));
    obj1->Something();
    std::unique_ptr<Base> obj2(MyFactory('b', nullptr, 1));
    obj2->Something();
}

【讨论】:

  • 有趣的是你有#include &lt;memory&gt;,但不要从MyFactory()返回std::unique_ptr&lt;Base&gt;...
  • 因为将返回的指针包装到您想要的任何 xxx_ptr 或分配给某个指针字段等更容易。
  • unique_ptr 可以移动分配给shared_ptr,我喜欢我们必须有意识地release() 才能将所有权移动到一个裸指针。不过,我想这只是一种偏好。
  • 是的,只是偏爱少写代码,写代码可读性强。从函数返回纯指针是一种更通用的方式,可以涵盖 this 函数的所有用例,包括模块的集成。
猜你喜欢
  • 2010-09-18
  • 2022-01-23
  • 1970-01-01
  • 2023-03-25
  • 2014-12-28
  • 2021-11-15
  • 1970-01-01
  • 2013-08-10
  • 1970-01-01
相关资源
最近更新 更多