Уничтожение проверенного типа во время компиляции - PullRequest
0 голосов
/ 01 апреля 2020

Вот моя цель: стереть информацию о типе, чтобы упростить доступ к объекту. Вот простой пример моей цели:

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 :: option), потому что типы известны в любом случае во время компиляции; мне не нужны просто дополнительные накладные расходы.

Причина, по которой я знаю, что это возможно, заключается в том, что 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 не будут работать в любом случае, поэтому не стесняйтесь публиковать ответ, который является контейнером, который использует стирание типа времени компиляции, так как я думаю, что это может быть намного проще, чем независимый тип.

Ответы [ 2 ]

2 голосов
/ 01 апреля 2020

Стирание типа - это концепция времени выполнения. По определению, он не может быть проверен во время компиляции. Если любой такой тип 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, или бросить исключение. Но вы не можете сделать это ошибка компиляции .

1 голос
/ 01 апреля 2020

К сожалению, я не могу ответить на ваш вопрос с тем же синтаксисом, что и в вопросе. Потому что, как утверждают другие, auto работает не так, как вы предполагаете. auto - это просто выведенный тип.

Если ему присваивается int, тип auto равен int. Однако это применимо только тогда, когда определяется тип auto. Любое исходящее назначение - это просто присвоение int, а не auto. Тип auto не является динамическим c, и его хранилище также не является динамическим c, поэтому auto нельзя использовать для хранения различных типов в std::vector.

Просто для добавления на другой ответ, надеюсь, помогая понять:


auto i = 10;

Тип i здесь int, а не auto.


auto b = true;

Тип i здесь bool, а не auto.


Тем не менее, я могу сделать все возможное, чтобы решить то, что, по моему мнению, является проблемой, с которой вы столкнулись.


Что делает этот ответ:

  1. Во время компиляции убедитесь, что доступ к переменной осуществляется через функцию с правильным типом параметра (минуя необходимость проверки типа).

  2. Предоставьте доступ набирать стертые данные без исключений (я думаю, это безопасно ...).

  3. Разрешить изменение данных.


Чего это не делает:

  1. Запуск во время компиляции из-за повторной интерпретации регистра.
  2. Разрешить присваивание напрямую через члены в std :: vector <>, хотя вы можете назначить из вызываемой функции доступа.

Как это работает:

Функция обратного вызова с типизированным параметром T & is type стирается и сохраняется как общая функция c. Хранение для этой функции - void (*) (), потому что указатели на функции не совпадают с обычными указателями void *, они часто имеют разные размеры.

Функция доступа с типизированным параметром настроена для вызова функцией с двумя типами параметров стертого указателя. Параметры преобразуются в их действительные типы в этой функции, типы известны, поскольку они присутствовали в конструкторе объекта base . Указатель на функцию, созданную в конструкторе в виде лямбды, сохраняется в указателе функции runner .

При запуске функции access бегун Функция с параметрами data и функцией acessor . Как только функция бегуна выполняется, она внутренне выполняет функцию accessor с параметром data , но на этот раз после того, как она приведена к правильному типу.

Когда требуется доступ вызывается стертая версия указанной выше функции, которая внутренне вызывает типизированную функцию. Я могу добавить поддержку лямбда-выражений в более поздней версии этого, но это уже довольно сложно, и я подумал, что я просто отправлю сейчас ...

Внутри базового класса существует класс деструктор . Это общий способ хранения уничтоженного типа деструктора, почти такой же, как метод Херба Саттерса . Это просто гарантирует, что данные, передаваемые базе, имеют свой деструктор.


Подход на основе кучи концептуально проще, вы можете запустить его здесь: 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;
}
...