类型擦除:检索值——编译时类型检查
Type erasure: Retrieving value - type check at compile time
我有一组有限的非常不同的类型,我想从中将实例存储在一个集合中,特别是地图。为此,我使用类型擦除惯用语,即。我有一个非模板化的基础 class,模板化的特定类型 class 从中继承:
struct concept
{
virtual std::unique_ptr<concept> copy() = 0; // example member function
};
template <typename T>
struct model : concept
{
T value;
std::unique_ptr<concept> copy() override { ... }
}
然后我将 unique_ptrs 存储到我的地图中的概念。为了检索值,我有一个模板化函数,它对指定类型进行动态转换。
template <typename T>
void get(concept& c, T& out) {
auto model = dynamic_cast<model<T>>(&c);
if (model == nullptr) throw "error, wrong type";
out = model->value;
}
我不喜欢这个解决方案的一点是,指定错误的 T 只会在运行时检测到。我真的很喜欢这在编译时完成。
我的选择如下所示,但我认为它们在这里无济于事:
通过将每种类型的自由函数指定为重载或模板函数来使用临时多态性,但我不知道在哪里存储结果。
使用 CRTP 是行不通的,因为这样基础 class 就需要模板化。
从概念上讲,我需要一个虚函数,它采用 class 的实例,结果将存储在该实例中。然而,由于我的类型根本不同,这个 class 需要模板化,这不适用于虚拟。
无论如何,我什至不确定这在逻辑上是否可行,但如果有办法做到这一点,我将非常高兴。
您不能对已擦除的类型进行编译时类型检查。这首先违背了类型擦除的全部要点。
但是,您可以通过提供擦除类型将与预期类型匹配的不变保证来获得同等级别的安全性。
当然,能不能行,还是要看你更高层次的设计。
这是一个例子:
class concept {
public:
virtual ~concept() {}
};
template<typename T>
struct model : public concept {
T value;
};
class Holder {
public:
template<typename T>
void addModel() {
map.emplace(std::type_index(typeid(T)), std::make_unique<model<T><());
}
template<typename T>
T getValue() {
auto found = types.find(std::type_index(typeid(T)));
if(found == types.end()) {
throw std::runtime_error("type not found");
}
// no need to dynamic cast here. The invariant is covering us.
return static_cast<model<T>*>(found->second.get())->value;
}
private:
// invariant: map[type] is always a model<type>
std::map<std::type_index, std::unique_ptr<concept>> types;
};
这里的强封装提供了几乎等同于编译时检查的安全级别,因为映射插入被积极保护以保持不变性。
同样,这可能不适用于您的设计,但它是处理这种情况的一种方式。
对于一组有限的类型,您的最佳选择是 variant
。您可以通过指定对每个变体采取的操作来最轻松地操作变体,然后它可以正确地对变体进行操作。沿着这些线的东西:
std::unordered_map<std::string, std::variant<Foo, Bar>> m;
m["a_foo"] = Foo{};
m["a_bar"] = Bar{};
for (auto& e : m) {
std::visit(overloaded([] (Foo&) { std::cerr << "a foo\n"; }
[] (Bar&) { std::cerr << "a bar\n"; },
e.second);
}
std::variant
是 c++17,但通常预先在实验命名空间中可用,您也可以使用 boost 中的版本。请参阅此处了解重载的定义:http://en.cppreference.com/w/cpp/utility/variant/visit(不幸的是,标准库没有提供一个小实用程序)。
当然,如果您希望某个键映射到特定类型,并且如果没有映射到特定类型则希望抛出错误,那么,仍然没有办法在编译时处理它。但这确实让您编写的访问者可以为变体中的每种类型做您想要的事情,在某种意义上类似于虚拟,但实际上不需要有一个公共接口或基础 class.
您的运行时检查发生在您退出类型擦除的位置。
如果要编译时检查操作,请将其移动到类型擦除边界内,或者导出足够的信息以便稍后类型擦除。
所以枚举类型,比如 std 变体。或者枚举算法,就像你复制的那样。您甚至可以混合使用它,例如用于存储各种类型的各种类型擦除子算法的变体。
这不支持任何类型多态性的任何算法;必须枚举两者之一才能在编译时解决问题,而不进行运行时检查。
我有一组有限的非常不同的类型,我想从中将实例存储在一个集合中,特别是地图。为此,我使用类型擦除惯用语,即。我有一个非模板化的基础 class,模板化的特定类型 class 从中继承:
struct concept
{
virtual std::unique_ptr<concept> copy() = 0; // example member function
};
template <typename T>
struct model : concept
{
T value;
std::unique_ptr<concept> copy() override { ... }
}
然后我将 unique_ptrs 存储到我的地图中的概念。为了检索值,我有一个模板化函数,它对指定类型进行动态转换。
template <typename T>
void get(concept& c, T& out) {
auto model = dynamic_cast<model<T>>(&c);
if (model == nullptr) throw "error, wrong type";
out = model->value;
}
我不喜欢这个解决方案的一点是,指定错误的 T 只会在运行时检测到。我真的很喜欢这在编译时完成。
我的选择如下所示,但我认为它们在这里无济于事:
通过将每种类型的自由函数指定为重载或模板函数来使用临时多态性,但我不知道在哪里存储结果。
使用 CRTP 是行不通的,因为这样基础 class 就需要模板化。
从概念上讲,我需要一个虚函数,它采用 class 的实例,结果将存储在该实例中。然而,由于我的类型根本不同,这个 class 需要模板化,这不适用于虚拟。
无论如何,我什至不确定这在逻辑上是否可行,但如果有办法做到这一点,我将非常高兴。
您不能对已擦除的类型进行编译时类型检查。这首先违背了类型擦除的全部要点。
但是,您可以通过提供擦除类型将与预期类型匹配的不变保证来获得同等级别的安全性。
当然,能不能行,还是要看你更高层次的设计。
这是一个例子:
class concept {
public:
virtual ~concept() {}
};
template<typename T>
struct model : public concept {
T value;
};
class Holder {
public:
template<typename T>
void addModel() {
map.emplace(std::type_index(typeid(T)), std::make_unique<model<T><());
}
template<typename T>
T getValue() {
auto found = types.find(std::type_index(typeid(T)));
if(found == types.end()) {
throw std::runtime_error("type not found");
}
// no need to dynamic cast here. The invariant is covering us.
return static_cast<model<T>*>(found->second.get())->value;
}
private:
// invariant: map[type] is always a model<type>
std::map<std::type_index, std::unique_ptr<concept>> types;
};
这里的强封装提供了几乎等同于编译时检查的安全级别,因为映射插入被积极保护以保持不变性。
同样,这可能不适用于您的设计,但它是处理这种情况的一种方式。
对于一组有限的类型,您的最佳选择是 variant
。您可以通过指定对每个变体采取的操作来最轻松地操作变体,然后它可以正确地对变体进行操作。沿着这些线的东西:
std::unordered_map<std::string, std::variant<Foo, Bar>> m;
m["a_foo"] = Foo{};
m["a_bar"] = Bar{};
for (auto& e : m) {
std::visit(overloaded([] (Foo&) { std::cerr << "a foo\n"; }
[] (Bar&) { std::cerr << "a bar\n"; },
e.second);
}
std::variant
是 c++17,但通常预先在实验命名空间中可用,您也可以使用 boost 中的版本。请参阅此处了解重载的定义:http://en.cppreference.com/w/cpp/utility/variant/visit(不幸的是,标准库没有提供一个小实用程序)。
当然,如果您希望某个键映射到特定类型,并且如果没有映射到特定类型则希望抛出错误,那么,仍然没有办法在编译时处理它。但这确实让您编写的访问者可以为变体中的每种类型做您想要的事情,在某种意义上类似于虚拟,但实际上不需要有一个公共接口或基础 class.
您的运行时检查发生在您退出类型擦除的位置。
如果要编译时检查操作,请将其移动到类型擦除边界内,或者导出足够的信息以便稍后类型擦除。
所以枚举类型,比如 std 变体。或者枚举算法,就像你复制的那样。您甚至可以混合使用它,例如用于存储各种类型的各种类型擦除子算法的变体。
这不支持任何类型多态性的任何算法;必须枚举两者之一才能在编译时解决问题,而不进行运行时检查。