В C можно присвоить указатель данных пустому указателю и затем вернуть его к исходному типу, этот указатель данных будет восстановлен. Языковой стандарт гарантирует, что такая трансформация не потеряет информацию. Это часто означает (не обязательно, но верно для большинства платформ), что размер указателя void совпадает с размером указателей данных. Таким образом, можно полагаться на эти факты, чтобы использовать пустые указатели в качестве общих указателей на гетерогенные типы, тогда как сами пустые указатели имеют одинаковый размер и представление. Например, у одного есть массив указателей void, элементы которого указывают на динамически размещаемые объекты различных типов. Создание такого массива делает некоторые вещи удобными.
Мой вопрос: как реализовать нечто подобное, общий тип указателя в C ++, который соответствует следующему: (предположим, что g_pointer — это имя класса)
Созданный из любых типов указателей, можно написать код как
g_pointer g_ptr = g_pointer(new T())
Восстановить оригинальный указатель
T* ptr = g_ptr.recover(), or
auto* ptr = g_tr.recover()
Обновление: Согласно некоторым комментариям, выше не может быть сделано в C ++, то что-то вроде
recover<Type>(g_ptr)
должно хватить, выбрасывая исключение Тип не совместим.
g_pointer может содержаться в std :: vector или в простом массиве, что в основном означает
sizeof(g_pointer) // a predetermined constant number,
(Обновление: это всегда так, при условии, что такой класс может быть правильно реализован, спасибо за указание.)
Я только что нашел boost :: any, заглянуть в его введение кажется предположить, что это может быть то, что я хочу, хотя это может быть не так. Так что любой, кто знаком с boost :: any, может комментировать.
Обновление: (ответ на некоторые комментарии)
Обновление: Спасибо @Caleth, std :: any отлично.
Это невозможно в C ++. Потому что тип выражения g_ptr.recover()
определяется во время компиляции, он не может хранить информацию базового типа, которая определяется во время выполнения.
Если вы можете терпеть такие выражения, как g_ptr.recover<T>()
Вы можете реализовать g_pointer
оборачивая void*
и const std::type_info&
которая хранит информацию о фактическом типе, на который указывает указатель, например,
class g_pointer {
public:
template <class T>
constexpr g_pointer(T *data) noexcept : _data(data), _object_type(typeid(T)) {}
template <class T>
T* recover() const {
if (typeid(T) == _object_type) return static_cast<T*>(_data);
else throw std::bad_cast{};
}
private:
void *_data;
const std::type_info &_object_type;
};
Обратите внимание на это g_pointer
ведет себя как необработанный указатель, а не как умный указатель, что означает, что он не владеет объектом, на который он указывает.
В реализации выше все еще есть дефект: const T*
не может быть неявно преобразовано в void*
, таким образом, общий указатель не может содержать const T*
, Для обработки const-квалификаторов вы можете изменить тип _data
в const void*
и использовать const_cast
при восстановлении. К тому же, recover
должен отказаться возвращать указатель на неконстантный объект из g_pointer
удерживая указатель на const объект. Тем не мение, typeid
Оператор игнорирует верхние константные квалификаторы, поэтому нам нужен дополнительный элемент данных для записи, указывает ли указатель на изначально константный объект.
class g_pointer {
public:
template <class T>
constexpr g_pointer(T *data) noexcept : _data(data),
_object_type(typeid(T)),
_is_const(std::is_const_v<T>)
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ change here
{
}
template <class T>
T* recover() const {
if (
typeid(T) != _object_type ||
(_is_const && !std::is_const_v<T>) // try to obtain T* while const T* is held
) {
throw std::bad_cast{};
}
else return static_cast<T*>(const_cast<void*>(_data));
// ^^^^^^^^^^^^^^^^^ change here
}
private:
const void *_data;
// ^^^^^ change here
const std::type_info &_object_type;
bool _is_const; // <-- record whether the pointer points to const T
};
Ничто не мешает вам использовать конструкции C, как void*
в C ++. Однако, как правило, его осуждают, потому что он может открыть дверь для различных ошибок, если код будет использоваться непреднамеренно или последствия указанных действий не будут полностью документированы.
При этом, по сути, вы просите обернуть void*
в классе, который затем может быть использован в std::vector
и затем доступ позже.
Вот некоторый код из фреймворка, который я написал некоторое время назад, чтобы добиться подобного эффекта:
#include <exception>
#include <typeinfo>
#include <map>
namespace so {
std::map<std::size_t, std::size_t> type_sizes;
template < typename T >
std::size_t type_id()
{
static char tid;
std::size_t sz = reinterpret_cast<std::size_t>(&tid);
so::type_sizes[sz] = sizeof(T);
return sz;
}
template < typename T >
inline std::size_t type_id(const T& t)
{
return so::type_id<T>();
}
template < typename T >
inline std::size_t type_id(const T *const t)
{
return so::type_id<T>();
}
template < typename T, typename C >
inline bool type_of()
{
return so::type_id<T>() == so::type_id<C>();
}
template < typename T, typename C >
inline bool type_of(const C& c)
{
return so::type_of<T, C>();
}
template < typename T, typename C >
inline bool type_of(const C *const c)
{
return so::type_of<T, C>();
}
template < typename T, typename C >
inline bool type_of(const T& t, const C& c)
{
return so::type_of<T, C>();
}
template < typename T, typename C >
inline bool type_of(const T *const t, const C *const c)
{
return so::type_of<T, C>();
}
class generic_ptr
{
public:
generic_ptr() : m_ptr(0), m_id(0) { }
template < typename T >
generic_ptr(T *const obj) :
m_ptr(obj), m_id(so::type_id<T>())
{
}
generic_ptr(const generic_ptr &o) :
m_ptr(o.m_ptr), m_id(o.m_id)
{
}
~generic_ptr()
{
this->invalidate();
}
static generic_ptr null()
{
return generic_ptr();
}
void invalidate()
{
this->m_ptr = 0;
this->m_id = 0;
}
template < typename T >
bool is_type() const
{
return this->m_id == so::type_id<T>();
}
template < typename T >
void gc()
{
delete ((T*)this->m_ptr);
this->invalidate();
}
bool valid() const
{
return (this->m_ptr != 0);
}
operator bool() const
{
return (this->m_ptr != 0);
}
bool operator!() const
{
return (!operator bool());
}
generic_ptr& operator=(const generic_ptr &o)
{
this->m_ptr = o.m_ptr;
this->m_id = o.m_id;
return *this;
}
template < typename T >
const generic_ptr& operator=(T *const obj)
{
this->m_ptr = obj;
this->m_id = so::type_id<T>();
return *this;
}
template < typename T >
operator T *const() const
{
if (this->m_id != so::type_id<T>()) {
throw std::bad_cast();
}
return static_cast<T *const>(
const_cast<void *const>(this->m_ptr)
);
}
template < typename T >
operator const T *const() const
{
if ((this->m_id != so::type_id<T>()) && (this->m_id != so::type_id<const T>())) {
throw std::bad_cast();
}
return static_cast<const T *const>(this->m_ptr);
}
operator void *const() const
{
return const_cast<void*>(this->m_ptr);
}
operator const void *const() const
{
return this->m_ptr;
}
bool operator==(const generic_ptr& o) const
{
return (this->m_ptr == o.m_ptr && this->m_id == o.m_id);
}
bool operator!=(const generic_ptr& o) const
{
return !(*this == o);
}
std::size_t hash() const
{
return this->m_id;
}
private:
const void* m_ptr;
std::size_t m_id;
};
}
Тогда использовать это:
#include <iostream>
#include <vector>
#include "generic_ptr.hpp"
class MyClass {
public:
MyClass() : m_val1(10), m_val2(20), m_val3(10), m_val4(2) {}
MyClass(int a, int b, int c, int d) : m_val1(a), m_val2(b), m_val3(c), m_val4(d) {}
friend std::ostream& operator<<(std::ostream& os, const MyClass& mc)
{
os << mc.m_val1 << " + " <<
mc.m_val2 << " + " <<
mc.m_val3 << " + " <<
mc.m_val4 << " = " <<
(mc.m_val1 + mc.m_val2 + mc.m_val3 + mc.m_val4);
return os;
}
private:
int m_val1;
int m_val2;
int m_val3;
int m_val4;
};
template < typename T >
void print(so::generic_ptr& g_ptr)
{
std::cout << "sizeof = " << so::type_sizes[g_ptr.hash()]
<< ", val = " << *((T*)g_ptr) << std::endl;
}
template < typename T >
void cleanup(so::generic_ptr& g_ptr)
{
delete ((T*)g_ptr);
}
int main(int argc, char* argv[])
{
std::vector<so::generic_ptr> items;
items.push_back(new int(10));
items.push_back(new double(3.14159));
items.push_back(new MyClass());
items.push_back(new char(65));
items.push_back(new MyClass(42,-42,65536,9999));
items.push_back(new int(999));
for (auto i : items) {
if (i.is_type<int>()) { print<int>(i); }
else if (i.is_type<char>()) { print<char>(i); }
else if (i.is_type<double>()) { print<double>(i); }
else if (i.is_type<MyClass>()) { print<MyClass>(i); }
}
int* i = (int*)items[0];
std::cout << "i = " << *i << std::endl;
*i = 500;
std::cout << "i = " << *i << std::endl;
try {
double* d = (double*)items[0];
std::cout << "d = " << *d << std::endl;
} catch (std::bad_cast& ex) {
std::cout << ex.what() << std::endl;
}
for (auto i : items) {
if (i.is_type<int>()) {
print<int>(i);
cleanup<int>(i);
} else if (i.is_type<char>()) {
print<char>(i);
cleanup<char>(i);
} else if (i.is_type<double>()) {
print<double>(i);
cleanup<double>(i);
} else if (i.is_type<MyClass>()) {
print<MyClass>(i);
cleanup<MyClass>(i);
}
}
return 0;
}
Конечно, вам все еще нужно знать тип и отслеживать память, но вы можете изменить код, чтобы справиться с этим; используя перегрузки оператора, вам не нужно recover
функционировать таким образом, вы можете просто сделать приведение, как в print
код: *((T*)g_ptr)
и может получить к нему доступ через необработанные указатели, как и до последнего for..each
заявление:
int* i = (int*)items[0];
*i = 500;
print<int>(items[0]);
Этот класс также имеет встроенное приведение неверных типов в случае, если вы пытаетесь привести между неверными типами:
try {
double* d = (double*)items[0];
// items[0] is an int, so this will throw a std::bad_cast
std::cout << "d = " << *d << std::endl;
} catch (std::bad_cast& ex) {
std::cout << ex.what() << std::endl;
}
Если честно, удобство может превзойти безопасность в этом случае, поэтому, если вам нужен массив типов, которые не согласованы или не могут быть определены с помощью базового класса, вам, возможно, придется переосмыслить то, что вы пытаетесь достичь в С ++ манера.
Я надеюсь, что это поможет вам получить некоторую ясность.