Как стать автором
Обновить

Побег из темницы типов. Реализуем работу с данными, тип которых определяется динамически

Время на прочтение15 мин
Количество просмотров31K
Всего голосов 16: ↑14 и ↓2+12
Комментарии27

Комментарии 27

НЛО прилетело и опубликовало эту надпись здесь
Swift офигенно кроссплатформенный
Похоже, но здесь нет ни одного шаблона снаружи (в отличие от type_erasure), а в статье как раз целый раздел посвящён тому, за что можно не любить шаблоны. Подход в статье также не призывает делать лишние cast'ы. К тому же можно свободно использовать наследники, не заморачиваясь только на object, как в Boost на any.
В самой статье шаблоны часто используются. Касты, наследники и общие типы не обязательны. Тем более, что с type_erasure получается значительно короче и лаконичнее:

BOOST_TYPE_ERASURE_MEMBER((has_name), Name, 0)
BOOST_TYPE_ERASURE_MEMBER((has_price), Price, 0)

namespace te = boost::type_erasure;

using Goods = boost::mpl::vector<te::copy_constructible<>, has_name<std::string(), te::_self>,
                                 has_price<float(), te::_self>, te::relaxed>;

using AnyGoods = te::any<Goods>;

// Протестируем

class Candies {
  public:
    Candies(std::string const &mName, float mPrice) : m_name(mName), m_price(mPrice) {}

    std::string Name() const { return m_name; }
    float Price() const { return m_price; }

  private:
    std::string m_name;
    float m_price;
};

class EmptyGoods {
  public:
    std::string Name() const { return "empty"; }
    float Price() const { return 0; }
};

TEST(AnyGoods_Should, contain_various_types) {
    AnyGoods goods1 = Candies("test", 123);
    AnyGoods goods2 = EmptyGoods();

    EXPECT_THAT(goods1.Name(), Eq("test"));
    EXPECT_THAT(goods1.Price(), Eq(123));
    EXPECT_THAT(goods2.Name(), Eq("empty"));
    EXPECT_THAT(goods2.Price(), Eq(0));
}
Ну если тебе нравится подход с вектором из кучи элементов в виде mpl-каши, то тоже вариант. Но вообще мой подход даёт возможность писать классы, просто описывая классы как обычно, просто они становятся юзабельными по значению, независимо от размера, прячут данные в реализацию и могут быть контейнерами для наследников. А так конечно можно и на void* всё то же самое сделать.
Я не понимаю зачем это делать для SQL.
Вот пример использования libpqxx
            const std::string query = "SELECT id, type_id, measurement_id FROM qoscfg.kpi";

            pqxx::work work(*m_connection, "GetKpii");
            const auto res = work.exec(query);

            for (auto i = res.begin(), r_end =res.end(); i != r_end; ++i)
            {
                size_t id = 0;
                (*i)[0].to(id);

                std::string type;
                (*i)[1].to(type);

                size_t mid = 0;
                (*i)[2].to(mid);

                const auto kpi = std::make_shared<Kpi>(id, type, m_measurement[mid]);
                list.push_back(kpi);
            }

Затем, что в большинстве случаев тебе надо запаковывать результат запроса в компактный набор значений, неопределённого на этапе компиляции типа, причём память, желательно, выделить однажды, а значениями всё забить так, чтобы с набором было удобно работать.
А это как вы системы запроектируете. Я проектирую слой работы с БД так, что бы таких случаев не было, у меня всегда типы известны.
Если делать SQL-конструктор на основе конструкций C++ или просто даже библиотеку общего пользования, то на этапе компиляции знать типы того, что придёт из БД не известно. То же касается и RPC.
У базы данных есть схема, в которой четко прописаны типы. Я привел пример с работой PostgreSQL, там в протоколе содержится информация о типах. Я разрабатываю базу в терминах предметной области и мне удобно опрерировать простым набором объектов, которые соответствуют таблицам.

В случае RPC должна быть некая конвенция о формате и типах данных. Например в случае с JSON-RPC я использую JSON схему для валидации.

SQL-конструкторы появляются в разработке своей ORM, или разработка всяческих конструкторов схем данных. Там действительно схема данных определяется пользователем, а не программистом.
Пользователь ORM-системы или SQL-генератора, наподобие LINQ является точно такой же программист, которому нужен удобный API, эффективность при выполнении инструкций и интуитивно понятный код в результате разработки. Разработчик библиотеки в свою очередь разумеется не знает заранее о том, что за типы придут с некой заранее неизвестной базы данных или с удалённого клиента RPC-протокола.
очередь разумеется не знает

А вот тут вы лукавите. Кое что разработчик API сделать может. Например разработчики libpqxx сделали такой метод, для извлечения результата:
template<typename T > bool pqxx::field::to (T & Obj) const

Отсюда имеем ситуацию:
  • я как пользователь знаю какие типы должен возвращать запрос;
  • разработчики API ввели ограничения и сказали явно указывайте тип;
  • во время выполнения библиотека знает какие типы пришли в запросе, какие типы указал пользователь и делает преобразование типов проверив на возможность такого действия.

и все это без таких сложностей, какие привели вы.
Неправда Ваша, смотрите, то что я предлагаю — это по сути аналог pqxx::field, только в более широком спектре использования. Никто не мешает сделать так:
template <typename value_type> value_type object::to() const;
Скажу больше, ровно так и нужно сделать. Обобщённый тип всегда должен давать возможность работать с содержимым в виде нативного значения.
К разговору о динамической типизации. Если мне нужно передать некоторое значение, которое будет приводиться к конкретному типу, основываясь на внутреннем состоянии, то как мне указать в функции, что аргумент не имеет определенного типа?

void SomeClass::DoSomething(??? * data) {
    switch(this->state) {
        case 1:
            int * value = reinterpret_cast<int>(data);
            // do something with value
            break;
        case 2:
            std::string * value = reinterpret_cast<std::string>(data);
            // do something with value
            break;
    }
}


Вариант использовать разные функции не подходит, так как принцип работы сложнее, чем в примере. Конечно, можно использовать структуру в которую и поместить data, но может есть более лаконичное решение.
Вопрос закрыт.
switch не расширяем
Описанное, больше похоже на реализацию pimpl. Ну, и на динамическую типизацию не особо тянет пока что.

Кстати, у Майерса в его книженции про 11ый есть ряд рекомендаций по типовой реализации pimpl, не совсеми я согласен, но почитать стоит.
Это не pimpl, я понимаю откуда такая аналогия. По сути это инкапсуляция интерфейса и подразумевает наследование. Pimpl — по сути просто класс с реализацией, с прокси-декоратором снаружи. У одного класса, в подходе Майерса, есть как правило класс двойник, и инкапсулировать прокси-класс с API может только его. Здесь подход именно на том, что наследование внутренних классов с данными инкапсулировано. Мы запросто можем положить в object любой его наследник.
Это именно она и есть, просто скрещена с Pimpl подходом. Можно чуть перестроить, чтобы было удобнее видеть параллели, но так как в статье удобнее использовать и читать понятнее.
Есть минус — невозможны взаимные ссылки по таким прокси-объектам.
class A { B b; }; class B { A a; };

По простым (или умным) указателям-то с forward-декларацией разруливается.

Из плюсов — можно операторы правильно перегружать.

Почему это невозможны, вполне можно напихать объекты классов друг в друга, только не в сами классы, а в их классы данных.
НЛО прилетело и опубликовало эту надпись здесь
Ну сам-то object с данными по умолчанию вполне себе всегда is_null(), а вот его наследники null далеко не всегда.
НЛО прилетело и опубликовало эту надпись здесь
Тоже вариант, но так нагляднее, не приходится везде во всех методах писать проверку на nullptr для m_data. Впрочем никто не запрещает создать шаблон от типа данных, наподобие nullable<typename data_type> и перегрузить там operator -> для проверки на nullptr указателя на data_type. Но опять же, теряется наглядность учебного материала.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий