如何处理 C++ 17 中变体中包含的类型的无意义方法

How to deal with pointless methods for types contained in a variant in c++ 17

我正在处理一个案例,其中需要某个容器 class 来保存自定义 classes 的变体(尤其是在向量中收集此类实例)。这些又是相互关联的。在代码示例中,此变体中的类型是 BirdFish,容器 class 是 AnimalContainer(有关完整的工作代码,请参见下文)。

不完整class概览:

using namespace std;
using uint = unsigned int;

class Animal {      
    protected:
        uint length_;
};

class Fish : public Animal {   
    private:
        uint depths_of_dive_;
};

class Bird : public Animal {   
    private:
        uint wing_span_;
};

class AnimalContainer {
    private:
        variant<Bird, Fish> the_animal_;
};

现在(忽略企鹅和其他一些鸟类),鸟类通常不能潜水,鱼没有翅膀(没听说过最少)。但是,代码应该提供使用 a.WingSpan() 通过 AnimalContainer class 的实例 a 请求 wing_span_ 的可能性,如果该动物是 Bird,以及使用 a.DepthOfDive()depth_of_dive_,应该是 Fish。此外,对于每个BirdFish,可以估计一个(生理上不现实的)体重,即a.EstimatedWeight()可以调用。

基本上为了避免编译错误,在Fishclass中添加了一个方法WingSpan(),在Birdclass中添加了DepthOfDive()

添加这些虚拟方法会变得非常麻烦,尤其是当涉及两个以上变体(此处 FishBird)时,或者当这些 classes 包含许多方法时。

一种可能性似乎使访问者过载特定 classes 并在所有其他情况下 return 一些警告(再次使用通用 lambda),但即使这稍微改进了过程, 这也很麻烦(见下面的第二个代码示例)。

您是否有建议如何以更全面的方式处理此问题,从而减少复制和粘贴?如果您对这个概念有一般性问题,也欢迎提出建议。

顺便说一下,动物容器 class 后来被放置在另一个 class 中,它可以引导用户避免意外调用虚拟函数。

第一个工作代码示例

#include <variant>
#include <iostream>

using namespace std;
using uint = unsigned int;


class Animal {
    public:
        Animal(uint length) : length_{length} {}

        uint Length() { return length_; }

    protected:
        uint length_;
};

class Fish : public Animal {
    public:
        Fish(uint length, uint depths_of_dive) : Animal(length), depths_of_dive_{depths_of_dive} {}

        uint DepthOfDive() { return depths_of_dive_; }
        uint EstimatedWeight() { return length_ * length_; }

        uint WingSpan() { cerr << "Usually fishes do not have wings... "; return 0; }

    private:
        uint depths_of_dive_;
};

class Bird : public Animal {
    public:
        Bird(uint length, uint wing_span) : Animal(length), wing_span_{wing_span} {}

        uint WingSpan() { return wing_span_; }
        uint EstimatedWeight() { return wing_span_ * length_; }

        uint DepthOfDive() { cerr << "Usually birds can not dive... "; return 0; }

    private:
        uint wing_span_;
};

class AnimalContainer {
    public:
        AnimalContainer(Bird b) : the_animal_{b} {}
        AnimalContainer(Fish f) : the_animal_{f} {}

        uint Length() {
            return visit([] (auto arg) { return arg.Length(); }, the_animal_);
        }
        uint WingSpan() {
            return visit([] (auto arg) { return arg.WingSpan(); }, the_animal_);
        }
        uint DepthOfDive() {
            return visit([] (auto arg) { return arg.DepthOfDive(); }, the_animal_);
        }
        uint EstimatedWeight() {
            return visit([] (auto arg) { return arg.EstimatedWeight(); }, the_animal_);
        }


    private:
        variant<Bird, Fish> the_animal_;
};

int main()
{
    Fish f(2,3);
    Bird b(2,3);

    AnimalContainer a_1(f);
    AnimalContainer a_2(b);

    cout << a_1.Length() << ' ' << a_1.WingSpan() << ' ' << a_1.DepthOfDive() << ' ' << a_1.EstimatedWeight() << endl; 
    cout << a_2.Length() << ' ' << a_2.WingSpan() << ' ' << a_2.DepthOfDive() << ' ' << a_2.EstimatedWeight() << endl;

    return 0;
}

第二个工作代码示例

#include <variant>
#include <iostream>

using namespace std;
using uint = unsigned int;


class Animal {
    public:
        Animal(uint length) : length_{length} {}

        uint Length() { return length_; }

    protected:
        uint length_;
};

class Fish : public Animal {
    public:
        Fish(uint length, uint depths_of_dive) : Animal(length), depths_of_dive_{depths_of_dive} {}

        uint DepthOfDive() { return depths_of_dive_; }
        uint EstimatedWeight() { return length_ * length_; }

        // no more dummy function

    private:
        uint depths_of_dive_;
};

class Bird : public Animal {
    public:
        Bird(uint length, uint wing_span) : Animal(length), wing_span_{wing_span} {}

        uint WingSpan() { return wing_span_; }
        uint EstimatedWeight() { return wing_span_ * length_; }

        // no more dummy function

    private:
        uint wing_span_;
};

template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

class AnimalContainer {
    public:
        AnimalContainer(Bird b) : the_animal_{b} {}
        AnimalContainer(Fish f) : the_animal_{f} {}

        uint Length() {
            return visit([] (auto arg) { return arg.Length(); }, the_animal_);
        }
        uint WingSpan() {
            return visit(overloaded { // now overloaded version
                [] (auto) { cerr << "This animal does not have wings... "; return uint(0); },
                [] (Bird arg) { return arg.WingSpan(); }}, the_animal_);
        }
        uint DepthOfDive() {
            return visit(overloaded { // now overloaded version
                [] (auto) { cerr << "This animal can not dive... "; return uint(0); },
                [] (Fish arg) { return arg.DepthOfDive(); }}, the_animal_);
        }
        uint EstimatedWeight() {
            return visit([] (auto arg) { return arg.EstimatedWeight(); }, the_animal_);
        }

    private:
        variant<Bird, Fish> the_animal_;
};

int main()
{
    Fish f(2,3);
    Bird b(2,3);

    AnimalContainer a_1(f);
    AnimalContainer a_2(b);

    cout << a_1.Length() << ' ' << a_1.WingSpan() << ' ' << a_1.DepthOfDive() << ' ' << a_1.EstimatedWeight() << endl; 
    cout << a_2.Length() << ' ' << a_2.WingSpan() << ' ' << a_2.DepthOfDive() << ' ' << a_2.EstimatedWeight() << endl;

    return 0;
}

首先,让我说,我很高兴看到新贡献者提出了关于设计的精心设计的问题。欢迎来到 Whosebug! :)

正如您正确提到的那样,您有两个选择:处理具体 classes 或容器中不存在的行为。让我们考虑这两种选择。

具体 classes

这通常是在继承和(动态)多态性、classic OOP 方法的帮助下完成的。在这种情况下,您甚至不应该使用 variant,因为 variant 用于不相关的 class。当你已经有了一个公共基础 class 时,使用它没有多大意义。

相反,在基础 class 中将您需要的整个接口定义为一组虚函数。一个好的做法是在层次结构的顶部有一个纯接口。然后你可以选择有一个中间(可能是抽象的)class 提供一些默认实现。这将使您不必为每个新派生的动物考虑不相关的概念,并避免一些代码重复。

代码可能如下所示(未经测试,只是向您展示概念):

// Pure interface on top of the hierarchy
class IAnimal {
    public:
        virtual ~IAnimal() = default.

        virtual uint Length() const = 0;
        virtual uint DepthOfDive() const = 0;
        virtual uint EstimatedWeight() const = 0;
        virtual uint WingSpan() const = 0;
};

// Intermediate class with some common implementations 
class Animal : public IAnimal {
    public:
        Animal(uint length) : length_{length} {}

        // We know how to implement this on this level already, so mark this final
        // Otherwise it won't have much sense to have the length_ field
        uint Length() const final { return length_; }

        // Some of these should be overridden by the descendants
        uint DepthOfDive() const override { cerr << "This creature can not dive... "; return 0; }
        uint WingSpan() const override { cerr << "This creature does not have wings... "; return 0; }

    private:
        uint length_;  // Better make it private
};

class Fish : public Animal {
    public:
        Fish(uint length, uint depths_of_dive) : Animal(length), depths_of_dive_{depths_of_dive} {}

        uint DepthOfDive() const { return depths_of_dive_; }
        uint EstimatedWeight() const { return Length() * Length(); }

    private:
        uint depths_of_dive_;
};

class Bird : public Animal {
    public:
        Bird(uint length, uint wing_span) : Animal(length), wing_span_{wing_span} {}

        uint WingSpan() const { return wing_span_; }
        uint EstimatedWeight() const { return wing_span_ * Length(); }

    private:
        uint wing_span_;
};

using AnimalContainer = std::unique_ptr<IAnimal>;

现在您可以直接使用指向基本接口的指​​针,而不是统一容器。经典。

容器

当您没有基础 class 时,拥有一个提供一些通用接口的统一容器可能很有意义。否则,您最好退回到上面描述的 classic OOP。所以,在这种情况下,你最好完全摆脱 Animal class 并按照你对所有特定动物的需要定义你需要的东西。

至于实现,您的方法实际上非常好,使用花哨的 overloaded 模式。我唯一可以建议你考虑的是使用一个通用的 lambda 作为内部有一堆 if constexpr 的访问者,因为这在某些情况下可能更容易阅读。但这真的取决于你的方法没有什么不好的。