问题描述
想象以下情况
- 您有一个指向变量的指针:
fooVar *
- 您可以使用如下函数获取变量的类型:
get_type(fooVar* foo)
返回一个 int 值bar
- 然后您可以在
enum
中查找返回值bar
所属的类型,例如1
->type_int
- 根据类型,您可以调用以下函数之一来获取值:
double get_double(fooVar*)
、int get_int(fooVar*)
、...
目标是
将这些函数(C 库的简化版本)包装到 CPP 类中。
问题是
- 我想存储所有变量的列表。
- 如何统一返回变量的当前值?
我尝试创建一个 BaseType 并从中派生其他类型,如下所示:
BaseClass
|--> IntClass - int getValue()
|--> DoubleClass - double getValue()
'--> BoolClass - bool getValue()
然后我可以为每个派生类创建一个具有正确返回类型的成员函数 .getValue()
(如上所示)。
由于我将指向所有变量的指针存储在类型为 BaseClass*
的列表中,因此我的编译器现在抱怨在我尝试调用它时未定义 getValue()
。
第二种解决方案可能是创建 double getAsDouble()
、bool getAsBool()
、int getAsInt()
,然后在类型不可转换或不适合时抛出错误。但这也感觉不对。
第三种解决方案可能是不仅存储指针,还存储它指向的(派生类的)类型。稍后我可以投射 BasePointer。但这真的是个好主意吗?
另一种解决方案可能涉及 std::variant
,但我认为这太过分了 - 对吧?
问题是:您将如何在 C++ 中做到这一点?有没有一种干净的方法来实现这一目标?
旁注:我在这里读到的关于 SO 的所有主题都没有真正适合,对我来说这感觉像是一个标准问题,所以我想学习一些专业程序员解决此类问题的方法。
解决方法
您似乎陷入了尝试重新实现虚函数和动态继承的困境。
因此,也许您能做的最直接的事情是拥有一个 virtual BaseType::getValue()
函数,该函数被特定于子类的 getValue()
覆盖。
但是现在,我们尽量避免使用原始指针数组 - 这需要显式分配和取消分配,并且容易出错(例如 - 如果抛出异常会发生什么?)。至少,让它像一个 std::vector<std::unique_ptr<BaseType>>
(或 std::shared_ptr
而不是 std::unique_ptr
)。如果您还没有听说过这些类似指针的类,请阅读:
What is a smart pointer and when should I use one?
除此之外 - 如果您事先知道可能的类型集,并且它不是很大 - 那么,正如您自己建议的那样 - std::variant
可能是一个合理的选择。 C++ 中的变体有点笨拙,但它们非常安全;一个你习惯了它们,它们足够方便。因此,就您而言:
using foo_var = std::variant<IntClass,DoubleClass,BoolClass>;
//...
auto foo_vars = get_array_of_foo_vars_somehow();
for(const foo_var& : foo_vars) {
your_complex_visit([]auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T,int>)
std::cout << "int with value " << arg << '\n';
else if constexpr (std::is_same_v<T,double>)
std::cout << "double with value " << arg << '\n';
else if constexpr (std::is_same_v<T,bool>)
std::cout << "bool with value " << arg << '\n';
else
static_assert(always_false_v<T>,"non-exhaustive visitor!");
},foo_var);
}
// Note: No need to `free()` anything when the array goes out of scope
现在,your_complex_visit()
有点像 std::visit()
,只是您查看的是 arg.getValue()
而不是 args()
。或者您可以在上面使用 std::visit
,但需要将类型更改为外部类型(IntClass
而不是 'int' 等)
第二种解决方案可能是创建 double getAsDouble()
、bool getAsBool()
、int getAsInt()
,然后在类型不可转换或不适合时抛出错误。但这也感觉不对。
发生错误时抛出异常的另一种方法是为 std::optional<double>
、std::optional<bool>
和 {{ 返回 std::optional<int>
、getAsDouble()
和 getAsBool()
1}},分别。这样,返回一个空的 getAsInt()
将表示失败 - 即没有这样的值可以检索 - 这不是错误。
例如,如果您有以下对应于 C 代码的声明:
std::option
您可以编写一个包装类,其中包含用户定义的 struct FooVar;
int get_type(const FooVar*);
int get_int(const FooVar*);
double get_double(const FooVar*);
#define INT_TYPE 1
#define DOUBLE_TYPE 2
和 FooVar *
转换运算符:
const FooVar *
请注意,由于转换运算符,您仍然可以直接使用 C API 与此包装类,例如,您可以使用 class FooVarWrapper {
FooVar *ptr_;
public:
// ...
operator FooVar*() noexcept { return ptr_; }
operator const FooVar*() const noexcept { return ptr_; }
// ...
};
对象作为参数调用原始函数 get_type()
,因为这将被隐式转换为存储的 FooVarWrapper
。
然后,您也可以将这些 getter 函数定义为非成员函数:
FooVar *
但是,您可能需要考虑将转换运算符标记为 std::optional<int> getAsInt(const FooVarWrapper& obj) {
if (INT_TYPE != get_type(obj))
return std::nullopt;
return get_int(obj);
}
std::optional<double> getAsDouble(const FooVarWrapper& obj) {
if (DOUBLE_TYPE != get_type(obj))
return std::nullopt;
return get_double(obj);
}
// ... similarly for getAsBool()
,以避免通过直接调用 explicit
或 std::optional
轻松绕过这些返回 get_int()
的函数。这样,每当您想将 get_double()
对象转换为存储的指针时,您都需要编写 static_cast<>
;转换不会隐式发生。