首页 > 解决方案 > c++ 我应该更喜欢联合还是异常

问题描述

我有一个联合的用例,但是和许多程序员一样,我觉得使用联合很难看。所以我尝试使用异常处理,而不是按预期的方式使用。我知道这会由于处理异常而导致一些时间损失。我的问题是:有没有一种干净的方法可以做到这一点?

这是代码,有联合,没有

//g++  7.4.0
#include <iostream>
using namespace std;
class C{ int i ; public : C(int i):i(i){cout << "C(" << i << ")\n";} void display() const { cout << "#C(" << i << ")\n"; } };
class D{ int i ; public : D(int i):i(i){cout << "D(" << i << ")\n";} void display() const { cout << "#D(" << i << ")\n"; } };
class E{ int i ; public : E(int i):i(i){cout << "E(" << i << ")\n";} void display() const { cout << "#E(" << i << ")\n"; } };

struct CDE { enum {C_t,D_t,E_t} kind; union { C c; D d; E e; }; CDE(){} };
CDE f(int i){ 
    CDE res;
    if( i==1 ) { res.kind = CDE::C_t; res.c = C(1); return res; }
    if( i==2 ) { res.kind = CDE::D_t; res.d = D(2); return res; }
    res.kind = CDE::E_t; res.e = E(i); return res;
}

void g(int i){
    if( i==1 ) throw C(1);
    if( i==2 ) throw D(2);
    throw E(i);
}

int main(){
    cout << "/** trace\n\nusing union\n";{
        CDE res = f(1);
        if (res.kind==CDE::C_t){ res.c.display(); }
        if (res.kind==CDE::D_t){ res.d.display(); }
        if (res.kind==CDE::E_t){ res.e.display(); }
    }cout << "\nusing exceptions\n";{
        try{
            g(1);
        }
        catch(const C& c){ c.display(); }
        catch(const D& d){ d.display(); }
        catch(const E& e){ e.display(); }
    }cout << "\nstop\n*/\n";
}

这是我得到的(明显的)痕迹

/** trace

using union
C(1)
#C(1)

using exceptions
C(1)
#C(1)

stop
*/

标签: c++union

解决方案


我强烈建议不要为此使用异常,它容易出错,而不是它们的用途。问题之一是可扩展性。使用异常,假设您添加了一种类型,您必须确保已将其添加到您的 try-catch 语句中。此外,这是一个坏习惯,因为它破坏了通常的代码流。(假设你在 之后添加一些东西g(),它永远不会被调用。另一个问题是,如果有实际的异常,你会在同一个 catch 块中将逻辑与错误处理混合在一起,这将变得更难阅读。或者你可能有代码在已经抛出异常时抛出异常,这将完全停止执行。

如果你想使用联合,你可以使用std::variant, 或者在你的枚举上使用 switch 语句(这比一个接一个地使用 ifs 更好。)

但是,在 C++ 和大多数面向对象的语言中,有一种更好的方法可以在这里实现你想要的,使用继承:

class C{ int i ; public : C(int i):i(i){cout << "C(" << i << ")\n";} void display() const { cout << "#C(" << i << ")\n"; } };
class D{ int i ; public : D(int i):i(i){cout << "D(" << i << ")\n";} void display() const { cout << "#D(" << i << ")\n"; } };
class E{ int i ; public : E(int i):i(i){cout << "E(" << i << ")\n";} void display() const { cout << "#E(" << i << ")\n"; } };

在你这里的代码中,我们可以看到所有这些类都有一个共同的接口(在这里理解,一个共同的“形状”,它们都有一个void display() const方法。

我们可以将所有这些类概括为与这个类“相同”(至少如果忽略它们方法中的逻辑)

class displayable {
    public:
        void display() const;
};

现在,这将是一种常见的“类型”。但是,我们希望所有三个类都能够实现或覆盖函数显示。让我们改变一下:

class i_displayable {
    public:
        virtual ~i_displayable(){}
        virtual void display() const = 0;

};

class displayable {
    public:
        virtual ~displayable() {}
        virtual void display() { std::cout << "displayable with default implementation" << std::endl;}
};

那么,这两者之间有什么区别: i_displayable声明display为纯虚拟成员。这意味着任何继承自的类i_displayable都必须实现display()

displayable声明display()为虚拟成员,并提供实现。这将允许继承类覆盖该函数。

我会马上解释为什么两者都声明了一个虚拟析构函数。

让我们暂时重写class C

class C : public displayable { 
    int i;
    public:
        C(int i): i(i) { std::cout << "C(" << i >> ")" << std::endl;}
        virtual ~C(){}

        void display() const override {
            std::cout << "#C(" << i << ")" << std::endl;
        }
}

所以,我们已经覆盖了显示函数(override关键字是可选的),并且我们已经说过C从 public 继承displayable

这意味着我们现在可以将任何指向class C实例的指针视为指向实例的指针displayable。由于函数 display 被标记为 virtual,当使用指针 dodisplayable时,函数 fromC将被调用,如果它存在,则调用 in ,displayable如果它不存在。

这实际上是使析构函数虚拟化的原因。您不想破坏 a displayable,而是实际实例(C在我们的例子中)

现在,假设您已在 和 上完成此操作,C您的调用代码可以重写为:DE

std::shared_ptr<displayable> f(int i){ 
    std::shared_ptr<displayable> res;

    if( i==1 ) { res = std::make_shared<C>(1); }
    else if( i==2 ) { res = std::make_shared<D>(2); }
    else { res = std::make_shared<E>(i); 

    return res;
}

int main(){
    cout << "/** trace\n\nusing union\n";{
        std::shared_ptr<displayable> res = f(1);
        res->display();
    }
}

推荐阅读