如何处理 C++ 17 中变体中包含的类型的无意义方法
How to deal with pointless methods for types contained in a variant in c++ 17
我正在处理一个案例,其中需要某个容器 class 来保存自定义 classes 的变体(尤其是在向量中收集此类实例)。这些又是相互关联的。在代码示例中,此变体中的类型是 Bird
和 Fish
,容器 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
。此外,对于每个Bird
和Fish
,可以估计一个(生理上不现实的)体重,即a.EstimatedWeight()
可以调用。
基本上为了避免编译错误,在Fishclass中添加了一个方法WingSpan()
,在Birdclass中添加了DepthOfDive()
。
添加这些虚拟方法会变得非常麻烦,尤其是当涉及两个以上变体(此处 Fish
和 Bird
)时,或者当这些 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
的访问者,因为这在某些情况下可能更容易阅读。但这真的取决于你的方法没有什么不好的。
我正在处理一个案例,其中需要某个容器 class 来保存自定义 classes 的变体(尤其是在向量中收集此类实例)。这些又是相互关联的。在代码示例中,此变体中的类型是 Bird
和 Fish
,容器 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
。此外,对于每个Bird
和Fish
,可以估计一个(生理上不现实的)体重,即a.EstimatedWeight()
可以调用。
基本上为了避免编译错误,在Fishclass中添加了一个方法WingSpan()
,在Birdclass中添加了DepthOfDive()
。
添加这些虚拟方法会变得非常麻烦,尤其是当涉及两个以上变体(此处 Fish
和 Bird
)时,或者当这些 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
的访问者,因为这在某些情况下可能更容易阅读。但这真的取决于你的方法没有什么不好的。