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

Posted

技术标签:

【中文标题】如何处理 C++ 17 中变体中包含的类型的无意义方法【英文标题】:How to deal with pointless methods for types contained in a variant in c++ 17 【发布时间】:2020-02-08 11:31:46 【问题描述】:

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

课程概览不完整:

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_;
;

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

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

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

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

您对如何以更全面、需要更少复制和粘贴的方式处理此问题有什么建议吗?如果您对此概念有一般性问题,也欢迎提出建议。

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

第一个工作代码示例

#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;

【问题讨论】:

请注意,变体通常是继承的替代。如果您有一个类层次结构,您通常通过(智能)指针访问基类的对象。变体对于完全不相关的类型很有用。 【参考方案1】:

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

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

具体类

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

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

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

// 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>;

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

容器

当您没有基类时,拥有一个提供一些通用接口的统一容器可能是有意义的。否则,您最好退回到上面描述的经典 OOP。因此,在这种情况下,您最好完全摆脱 Animal 类,并以所有特定动物所需的方式定义您需要的内容。

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

【讨论】:

感谢您的客气话和评论。原则上,我喜欢使用纯继承(没有变体)的想法。但是,它需要使用指针功能来处理我想不惜一切代价避免的类 AnimalContainer(也因为该库应该尽可能地对用户友好)。我尝试编写一个类 AnimalContainer 作为包装器,它只包含一个指向 IAnimal 对象的智能指针。然而,我遇到了迄今为​​止无法解决的分段错误。 您对指针有什么顾虑?它们是 c++ 的基础。 我正在为其编写代码的库是基于一个非常简单的基于类的接口。就我个人而言,我并不反对指针,尽管(对于 c++17 来说是新手)它们似乎从用户级别消失了。我也觉得指针有时处理起来有点麻烦。虽然非常有用,但出于这个原因,我更喜欢将指针保留在子方法中并隐藏在类后面。 “它们似乎从用户级别消失了” - 这不是真的。自 C++11 以来发生的变化是原始指针 T* 不应拥有它指向的对象。 “在子方法中保持本地指针并隐藏在类后面” - 这是一个有问题的方法。确保您没有使用其他语言转换 C++。

以上是关于如何处理 C++ 17 中变体中包含的类型的无意义方法的主要内容,如果未能解决你的问题,请参考以下文章

字符串类型数组的指针运算,C++如何处理这个?

如何处理张量流中0-1范围之外的输入?

c++ 编译器如何处理条件中声明的类型?

SQL Server 如何处理非聚集索引中的包含列?

如何处理 Android 中远程视图的异常(自定义小部件或自定义通知)?

数据库中字段的数据格式为 1,2,3 时如何处理数据