[英]Compile-time Validated Type Erasure
这是我的目标:擦除类型信息以简化对象访问。 这是我的目标的一个简单示例:
class magic;
magic m = std::string("hello"); // ok: m now stores a string
m = 32; // error: m is supposed to be a string
m += " world"; // ok: operator for this exists
您可能已经注意到,这基本上类似于 auto 关键字。
要继续,它最好也不要根据类型(例如使用指针)改变其大小。 这样我就可以使用容器了。
std::vector<magic> vec; // homogeneous
vec.emplace_back(8);
vec.emplace_back(std::string("str"));
vec[0] = 4; // ok
vec[1] = 2; // no way, jose. compile error here because vec[1] is a string
这个想法是它必须是编译时的(而不是像 std::any 或 std::variant 那样的运行时),因为无论如何这些类型在编译时都是已知的; 这只是我不需要的额外开销。
我知道这是可能的原因是因为 auto 已经完成了这项工作。 我只需要一个某种类型的容器,它的功能类似于auto*
,它实际上在编译时验证操作,以节省开销和非常乏味的冗余编程。
这是我计划使用它的方式(警告:伪代码错误)
struct base
{
auto* p;
};
struct child: base<int> // child implements base as an int
{
// use p and implement whatever functions are necessary
};
std::vector<base> vec;
vec.emplace_back(child());
vec[0] = 20;
如果您愿意,如果您担心“密钥”访问会根据推回的内容而更改,请假装它是地图而不是向量。 但是我有一种预感,无论如何 stl 容器都不会工作,所以请随意发布一个使用编译时类型擦除的容器的答案,因为我认为这可能比独立类型容易得多。
类型擦除是一个运行时概念。 根据定义,它不能在编译时验证。 如果任何这样的magic
类型可能存在,它就无法在编译时确定vec[0] = 4
是可以的,而vec[1] = 2
不是。
我知道这是可能的原因是因为 auto 已经完成了这项工作。
不,不是的。 auto
是一种语法结构,它使 C++ 根据表达式的(编译时确定的)类型推导出变量的(编译时确定的)类型。 auto
存在于编译器中,而不是运行时中。
你想要的是在运行时发生的事情。 虽然任何特定vec[X]
的类型是在编译时确定的,但它的值是一个运行时属性。 您希望该值以某种方式使赋值成为编译错误。 这是不可能的。
这就是tuple
使用get<X>
而不是get(X)
。 索引必须是编译时常量,这允许get<X>
的类型对于元组中的每个特定X
可能不同。
类型的属性,例如可从整数赋值,是编译时构造。 也就是说,要么vec[X] = 4
是格式良好的代码,要么不是; 根据X
和vec
的内容,不可能使它有时格式正确,有时又不是。 您可以将其设为 UB,也可以抛出异常。 但是你不能让它成为一个编译错误。
不幸的是,我无法使用与问题中相同的语法来回答您的问题。 因为正如其他人所说, auto
工作方式与您的假设不同。 auto
只是一个推导的类型。
如果它被分配了一个int
,那么auto
的类型是int
。 但是,这仅适用于推导 auto 类型的情况。 任何正在进行的分配都只是分配给int
,而不是分配给auto
。 auto
的类型不是动态的,它的存储也不是动态的,这就是为什么 auto 不能用于在std::vector
存储各种不同的类型。
只是添加到另一个答案中,希望有助于理解:
auto i = 10;
i
这里的类型是int
而不是auto
。
auto b = true;
i
这里的类型是bool
而不是auto
。
但是,我可以尽力解决我认为您面临的问题。
这个答案的作用:
在编译时确保对变量的访问是通过具有正确参数类型的函数完成的(绕过检查类型的需要)。
无一例外地提供对类型擦除数据的访问(我认为它是安全的......)。
允许修改数据。
这不做什么:
这个怎么运作:
具有类型化参数 T& 的回调函数被类型擦除并存储为泛型函数。 这个函数的存储是 void (*)() 因为函数指针与普通的 void * 指针不同,它们通常有不同的大小。
带有类型参数的访问器函数设置为由带有两个类型擦除指针参数的函数调用。 参数在此函数中被转换为它们的实际类型,这些类型是已知的,因为它们存在于基础对象的构造函数中。 指向在构造函数中作为 lambda 创建的函数的指针存储在runner函数指针中。
当功能访问运行时,与参数数据和acessor功能转轮功能。 一旦 runner 函数被执行,它就会在内部执行带有参数数据的访问器函数,但这次是在它被转换为正确的类型之后。
当需要访问时,调用上述函数的类型擦除版本,该版本在内部调用类型化函数。 我可以在以后的版本中添加对 lambdas 的支持,但它已经非常复杂了,我想我现在就发布......
在基类内部存在一个析构函数类。 这是存储类型擦除析构函数的通用方法,与Herb Sutters 方法几乎相同。 这只是确保提供给基础的数据运行其析构函数。
基于堆的方法在概念上更简单,您可以在此处运行它: https : //godbolt.org/z/cb-a6m
基于堆栈的方法可能更快,但有更多限制: https : //godbolt.org/z/vxS4tJ
基于代码堆的(更简单的)代码:
#include <iostream>
#include <memory>
#include <utility>
#include <vector>
template <typename T>
struct mirror { using type = T; };
template <typename T>
using mirror_t = typename mirror<T>::type;
struct destructor
{
const void* p = nullptr;
void(*destroy)(const void*) = nullptr;
//
template <typename T>
destructor(T& data) noexcept :
p{ std::addressof(data) },
destroy{ [](const void* v) { static_cast<T const*>(v)->~T(); } }
{}
destructor(destructor&& d) noexcept
{
p = d.p;
destroy = d.destroy;
d.p = nullptr;
d.destroy = nullptr;
}
destructor& operator=(destructor&& d) noexcept
{
p = d.p;
destroy = d.destroy;
d.p = nullptr;
d.destroy = nullptr;
return *this;
}
//
destructor() = default;
~destructor()
{
if (p and destroy) destroy(p);
}
};
struct base
{
using void_ptr_t = void*; // Correct size for a data pointer.
using void_func_ptr_t = void(*)(); // Correct size for a function pointer.
using callback_t = void (*)(void_func_ptr_t, void_ptr_t);
//
void_ptr_t data;
void_func_ptr_t function;
callback_t runner;
destructor destruct;
//
template <typename T>
constexpr base(T * value, void (*callback)(mirror_t<T>&)) noexcept :
data{ static_cast<void_ptr_t>(value) },
function{ reinterpret_cast<void_func_ptr_t>(callback) },
runner{
[](void_func_ptr_t f, void_ptr_t p) noexcept
{
using param = T&;
using f_ptr = void (*)(param);
reinterpret_cast<f_ptr>(f)(*static_cast<T*>(p));
}
},
destruct{ *value }
{}
//
constexpr void access() const noexcept
{
if (function and data) runner(function, data);
}
};
struct custom_type
{
custom_type()
{
std::cout << __func__ << "\n";
}
custom_type(custom_type const&)
{
std::cout << __func__ << "\n";
}
custom_type(custom_type &&)
{
std::cout << __func__ << "\n";
}
~custom_type()
{
std::cout << __func__ << "\n";
}
};
//
void int_access(int & a)
{
std::cout << "int_access a = " << a << "\n";
a = 11;
}
void string_access(std::string & a)
{
std::cout << "string_access a = " << a << "\n";
a = "I'm no longer a large string";
}
void custom_access(custom_type& a)
{
}
int main()
{
std::vector<base> items;
items.emplace_back(new std::string{ "hello this is a long string which doesn't just sit in small string optimisations, this needs to be tested in a tight loop to confirm no memory leaks are occuring." }, &string_access);
items.emplace_back(new custom_type{}, &custom_access);
items.emplace_back(new int (10), &int_access);
//
for (auto& item : items)
{
item.access();
}
for (auto& item : items)
{
item.access();
}
//
return 0;
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.