【问题标题】:Create distinct type for class with specific member variable value为具有特定成员变量值的类创建不同类型
【发布时间】:2018-05-13 14:50:55
【问题描述】:

给定一个类,该类具有一些定义类类型的枚举,如下例所示:

class Fruit {
 public:

   enum class FruitType {
      AppleType = 0,
      OrangeType = 1,
      BananaType = 2,
   };
   Fruit(FruitType type) : type_(type) {}
   FruitType fruit_type() const { return type_; }

 private:
   FruitType type_;
};

以及从它派生的共享相同枚举的类:

class DriedFruit : public Fruit {
 public:
  // Some Dried specific methods.
};

是否可以通过每个特定的枚举值为 Fruit 和 DryFruit 定义不同的类型:

class Apple   // Fruit with FruitType = AppleType
class Orange  // Fruit with FruitType = OrangeType
class Banana  // Fruit with FruitType = BananaType
class DriedApple   // DriedFruit with FruitType = AppleType
class DriedOrange  // DriedFruit with FruitType = OrangeType
class DriedBanana  // DriedFruit with FruitType = BananaType

所以 Apple、Orange 和 Banana 3 个类是不同的类型,DriedApple、DriedOrange、DriedBanana 3 个类是不同的类型。

我的问题有点类似于How to define different types for the same class in C++,只是我想将有关类类型的信息显式存储为类中的枚举成员变量,并为所有不同类型提供一个公共基类。

最有效的方法是什么?

编辑: 主要用例如下 - 在我的应用程序中,有某些方法只期望 Apple 作为输入,或者只期望 Orange 作为输入,还有许多方法不关心它是哪种水果。

将 Fruit 传递给只期望 Apple 的方法感觉不安全/晦涩难懂,同时有许多方法不关心它是哪种类型,因此拥有 3 个不同的类型也不是一个好的选择。

主要工作流程如下: 从一些输入参数构建一个水果,然后 传递它并将其作为水果处理,然后在某个时候 如果是 Apple,则将 Fruit 转换为具体的 Apple 类型,并进一步处理它,从那时起将其类型限制为 Apple。

【问题讨论】:

  • 你链接的问题的答案不是回答这个问题吗?
  • 使type_ 受保护并让每个派生类在构造函数中适当设置它有什么问题吗??
  • 这看起来有点 XY 问题。为什么您真的需要为Fruit 的特定实例提供不同类型?在公共类中实现的专用接口?后者闻起来是设计缺陷。
  • @user0042 - 很可能是 XY 问题 - 我已经扩展了我的问题以提示用例
  • @Ilya Kobelevskiy 我认为您需要扩展此位 then at some point if it is an Apple, further process it, restricting it type to an Apple from that point onwards. 您希望它在代码中看起来如何?因为我认为这可能是你的“X”,而这个枚举是你的“Y”。

标签: c++ c++11 inheritance types


【解决方案1】:

关于要求,您的问题非常抽象。

虽然您编辑的澄清说明了一种方式

主要用例如下——在我的应用程序中,有一些方法只期望Apple作为输入,或者只期望Orange作为输入,还有许多方法不关心它是什么水果。

我正在考虑基于接口和标签接口的完全不同的系统
(查看完整的Live Demo)。

首先为所有水果定义一个通用接口:

// A basic interface common for all fruits
struct IFruit {
  virtual ~IFruit() {}
  virtual std::string category() const = 0;
  virtual std::string common_name() const = 0;
  virtual std::string botanical_name() const = 0;
};

// An overload for the output operator is just nifty
std::ostream& operator<<(std::ostream& os, const IFruit& fruit) {
    os << "Category       : " << fruit.category() << std::endl;
    os << "Common Name    : " << fruit.common_name() << std::endl;
    os << "Botanical Name : " << fruit.botanical_name() << std::endl;
    return os;
}

定义标签接口以区分您的特定类型(苹果、橙子):

// Tag interfaces to distinguish (not necessarily empty)
struct IApple : public IFruit {
  virtual ~IApple() {}
};

struct IOrange : public IFruit {
  virtual ~IOrange () {}
};

这些应该需要隐式实现IFruit 接口。


现在您可以提供一个抽象基类,它实现了IFruit 接口。
这个基类是抽象的,因为构造函数隐藏在public范围内,需要由继承类构造函数调用:

// Abstract base class implementation
template<class TagInterface>
class FruitBase : public TagInterface {
protected:
      std::string category_;
      std::string common_name_;
      std::string botanical_name_;

      FruitBase ( const std::string& category
                , const std::string& common_name
                , const std::string botanical_name) 
          : category_(category), common_name_(common_name)
          , botanical_name_(botanical_name) 
      {}

public:
      virtual ~FruitBase () {}
      virtual std::string category() const { return category_; }
      virtual std::string common_name() const { return common_name_; }
      virtual std::string botanical_name() const { return botanical_name_; }
};

根据需要添加额外的标签接口

 struct IDriedApple : public IApple {
     virtual ~IDriedApple() {}
     virtual int rest_humidity() const = 0;
 };

现在您可以使用非常窄的类定义创建具体实现:

// Concrete apples
struct Boskop : public FruitBase<IApple> {
public:
     Boskop() : FruitBase("Apples","Boskop","Malus domestica 'Belle de Boskoop'") {}
};

struct Braeburn : public FruitBase<IApple> {
public:
     Braeburn() : FruitBase("Apples","Braeburn","Malus domestica") {}
};

// Concrete oranges
struct Valencia : public FruitBase<IOrange> {
public:
     Valencia() : FruitBase("Oranges","Valencia","Citrus × sinensis") {}
};

struct Navel : public FruitBase<IOrange> {
public:
     Navel() : FruitBase("Oranges","Navel","Citrus × sinensis") {}
};

我假设你的函数声明专门用于ApplesOranges

void aFunctionThatTakesOnlyApples(IApple& anApple) {
    std::cout << "This is an apple:" << std::endl;
    std::cout << anApple;
}

void aFunctionThatTakesOnlyOranges(IOrange& anOrange) {
    std::cout << "This is an orange:" << std::endl;
    std::cout << anOrange << std::endl;
}

这是一个简单的模板函数,用于查询IFruit 的已知实例以实现特定的标签接口: 模板 TagInterface* queryTagInterface(IFruit*fruit) { 返回dynamic_cast(水果); }


这就是你如何在行动中使用所有这些:

int main() {
    std::vector<std::unique_ptr<IFruit>> allFruits;
    allFruits.push_back(std::make_unique<Boskop>());
    allFruits.push_back(std::make_unique<Braeburn>());
    allFruits.push_back(std::make_unique<Valencia>()); 
    allFruits.push_back(std::make_unique<Navel>());
    for(auto& fruit : allFruits) {
        if(IApple* anApple = queryTagInterface<IApple>(fruit.get())) {
            aFunctionThatTakesOnlyApples(*anApple);
        }
        if(IOrange* anOrange = queryTagInterface<IOrange>(fruit.get())) {
            aFunctionThatTakesOnlyOranges(*anOrange);
        }
        std::cout << "-----------------------------------------------" << std::endl;
    }    
}

将 Fruit 传递给只期望 Apple 的方法感觉不安全/晦涩难懂,同时有许多方法不关心它是哪种类型,因此拥有 3 个不同的类型也不是一个好的选择。

我应该指出,我仍然不明白是什么让 ApplesOranges 如此不同的 Fruits 真正值得拥有自己的类型.但是,这可能适用于多态类设计的不那么抽象的隐喻,并且对具体的类层次结构设计很有用。

【讨论】:

    【解决方案2】:

    这是你想做的吗?

    enum class FruitType 
    {
        AppleType = 0,
        OrangeType = 1,
        BananaType = 2,
    };
    
    class Fruit 
    {
    public:
    
        virtual FruitType fruit_type() const = 0;
    };
    
    class Apple: public Fruit 
    {
    public:
    
        FruitType fruit_type() const override { return FruitType::AppleType; }
    };
    
    class Orange : public Fruit 
    {
    public:
    
        FruitType fruit_type() const override { return FruitType::OrangeType; }
    };
    
    class Banana : public Fruit 
    {
    public:
    
        FruitType fruit_type() const override { return FruitType::BananaType; }
    };
    
    int main()
    {
        Fruit *somefruit = new Apple;
    
        std::cout << "Is Apple? " << std::boolalpha << (somefruit->fruit_type() == FruitType::AppleType) << std::endl;
        std::cout << "Is Orange? " << std::boolalpha << (somefruit->fruit_type() == FruitType::OrangeType) << std::endl;
        std::cout << "Is Banana? " << std::boolalpha << (somefruit->fruit_type() == FruitType::BananaType) << std::endl;
    
        return 0;
    }
    

    打印:

    Is Apple? true
    Is Orange? false
    Is Banana? false
    

    【讨论】:

    • +1。谢谢,这可行,但问题是 Fruit 已经是我的应用程序中的一个基类,所以这种方法会导致我想避免的多重继承。
    • @IlyaKobelevskiy 我不明白这种方法如何导致多重继承。你能分享一个例子吗?无论如何,如果这个解决方案是正确的,那么对于任何可以想象的解决方案似乎都是正确的。虽然也许我没明白你的意思。需要明确的是,A 从 B 继承和 B 从 C 继承不是多重继承,通常不是问题。
    • @FrançoisAndrieux 问题是在我的应用程序中还有DriedFruit,它是从FruitType 正交概念派生的Fruit。我需要处理 Fruit 和 DryFruit 类型的对象,其中 DryFruit 是 Fruit 加上其他东西。有了这个解决方案,Fruit 变得抽象,因此 DriedFruit 也变得抽象 - 我们现在需要定义 DredApple、DriedOrange 和 DriedBanana 以便能够实例化 DredFruit 的具体实例。
    • @IlyaKobelevskiy 由于DriedFruit 似乎并不关心具体的水果类型,您可以在Fruit 和这些具体的水果类型之间引入一个中间类型。例如,AppleBanada 可以从继承 Fruit 的新类型 SpecificFruit 继承。那么FruitDriedFruit 可以保持不变。或者,您可以为 DriedFruit 实现 fruit_type,以便它为该类型返回一个不同的枚举值,就像所有其他水果类型一样。
    • @IlyaKobelevskiy:我不希望虚拟继承,但它会处理这种情况。如果DriedFruit 虚拟继承自Fruit,而DriedApple 虚拟继承自FruitDriedFruit,则覆盖Fruit 抽象方法的DriedApple 将适用于FruitDriedFruit . Live Demo
    【解决方案3】:

    最有效的方法是什么?

    您可以使用非类型模板参数:

    enum class FruitType {
       AppleType = 0,
       OrangeType = 1,
       BananaType = 2,
    };
    
    template <FruitType F>
    class Fruit {
     public:
       FruitType fruit_type() const { return F; }
    };
    
    using Apple = Fruit<FruitType::AppleType>;
    using Banana = Fruit<FruitType::BananaType>;
    

    您是否需要实际的基类取决于您。为某些FruitTypes 提供模板特化也可能就足够了。

    【讨论】:

    • 这个答案很好,但是AppleBanana 不共享一个共同的基本类型,正如问题中所问的那样。
    • @FrançoisAndrieux 使用 CRTP 和继承很容易解决。
    • @user0042 你不需要 CRTP,你只需要 Fruit 从一个公共基类继承。
    • 问题是作者的真正意图是什么。在某种意义上,通用模板可能是共同的基础,与它的偏差可能会在专业化方面实现。
    • @FrançoisAndrieux 我知道,但这对我来说是最自然的情况。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-08-17
    • 2012-05-25
    • 1970-01-01
    • 2021-07-23
    相关资源
    最近更新 更多