Ответ 1
Цель реализации
Мы собираемся написать автоматический переводчик. Допустим, у нас есть объект, представляющий наш проводной формат:
JsonObject wire_data;
Для удобства мы можем представить, что наш JsonObject
имеет add_field
член add_field
:
wire_data.add_field("name", "value");
Однако фактический интерфейс JsonObject
на самом деле не имеет значения, и остальная часть этого поста не зависит от его реализации каким-либо конкретным способом.
Мы хотим иметь возможность написать эту функцию:
template<class Car>
void add_car_info(JsonObject& object, Car car) {
// Stuff goes here
}
со следующими ограничениями:
- Если у
Car
есть поле, например,Car::getMake()
, наша функцияadd_car_info
должнаadd_car_info
добавить это поле в объект json - Если у
Car
нет поля, наша функция не должна ничего делать. - Наша реализация не должна полагаться на то, что
Car
является производным от чего-либо или является базовым классом чего-либо - Наша реализация должна упростить добавление новых полей, не нарушая обратной совместимости.
Пример с четырьмя независимыми классами автомобилей
Допустим, у вас есть четыре класса автомобилей. Никто из них не разделяет базовый класс; какие поля они выставляют, меняется; и вы можете добавить больше классов автомобилей в будущем.
struct Car1
{
std::string getMake() { return "Toyota"; }
std::string getModel() { return "Prius"; }
int getYear() { return 2013; }
};
struct Car2
{
std::string getMake() { return "Toyota"; }
int getYear() { return 2017; };
};
struct Car3
{
std::string getModel() { return "Prius"; }
int getYear() { return 2017; }
};
struct Car4
{
long long getSerial() { return 2039809809820390; }
};
Сейчас,
JsonObject wire_data;
Car1 car1;
add_field(wire_data, car1);
Должны быть эквивалентны
Car1 car1;
wire_data.add_field("car make", car1.getMake());
wire_data.add_field("car model", car1.getModel());
wire_data.add_field("year", car1.getYear());
В то время как
Car2 car2;
add_field(wire_data, car2);
Должен быть эквивалентен
Car2 car2;
wire_data.add_field("car make", car2.getMake());
wire_data.add_field("year", car2.getYear());
Как мы реализуем add_car_info
универсальным способом?
Выяснить, какие машины имеют какие поля, является сложной задачей, особенно потому что C++
не имеет динамического отражения, но мы можем сделать это, используя статическое отражение (и это будет более эффективным)!
Сейчас я собираюсь делегировать функциональность объекту, представляющему переводчик.
template<class Car>
void add_car_info(JsonObject& wire_object, Car car) {
auto translator = getCarTranslator();
// This lambda adds the inputs to wire_object
auto add_field = [&](std::string const& name, auto&& value) {
wire_object.add_field(name, value);
};
// Add the car fields.
translator.translate(add_field, car);
}
Похоже, что объект- translator
просто пинает, может в будущем, однако наличие объекта- translator
облегчит написание translator
для вещей, отличных от автомобилей.
Как мы реализуем волшебный переводчик?
Давайте начнем с getCarTranslator
. Что касается автомобилей, мы можем заботиться о четырех вещах: модель, год и серийный номер.
auto getCarTranslator() {
return makeTranslator(READ_FIELD("car make", getMake()),
READ_FIELD("car model", getModel()),
READ_FIELD("year", getYear()),
READ_FIELD("serial", getSerial()));
}
Здесь мы используем макрос, но я обещаю, что он единственный, и это не сложный макрос:
// This class is used to tell our overload set we want the name of the field
class read_name_t
{
};
#define READ_FIELD(name, field) \
overload_set( \
[](auto&& obj) -> decltype(obj.field) { return obj.field; }, \
[](read_name_t) -> decltype(auto) { return name; })
Мы определяем перегрузку, установленную для двух лямбд. Один из них получает поле объекта, а другой - имя, используемое для сериализации.
Реализация набора перегрузки для лямбд
Это довольно просто. Мы просто создаем класс, который наследует обе лямбды:
template <class Base1, class Base2>
struct OverloadSet
: public Base1
, public Base2
{
OverloadSet(Base1 const& b1, Base2 const& b2) : Base1(b1), Base2(b2) {}
OverloadSet(Base1&& b1, Base2&& b2)
: Base1(std::move(b1)), Base2(std::move(b2))
{
}
using Base1::operator();
using Base2::operator();
};
template <class F1, class F2>
auto overload_set(F1&& func1, F2&& func2)
-> OverloadSet<typename std::decay<F1>::type, typename std::decay<F2>::type>
{
return {std::forward<F1>(func1), std::forward<F2>(func2)};
}
Реализация класса переводчика с использованием чуть-чуть SFINAE
Первый шаг - создать класс, который читает отдельное поле. Он содержит лямбду, которая делает чтение. Если мы можем применить лямбду, мы применяем ее (читая поле). В противном случае мы его не применяем, и ничего не происходит.
template <class Reader>
class OptionalReader
{
public:
Reader read;
template <class Consumer, class Object>
void maybeConsume(Consumer&& consume, Object&& obj) const
{
// The 0 is used to dispatch it so it considers both overloads
maybeConsume(consume, obj, 0);
}
private:
// This is used to disable maybeConsume if we can't read it
template <class...>
using ignore_t = void;
// This one gets called if we can read the object
template <class Consumer, class Object>
auto maybeConsume(Consumer& consume, Object& obj, int) const
-> ignore_t<decltype(consume(read(read_name_t()), read(obj)))>
{
consume(read(read_name_t()), read(obj));
}
// This one gets called if we can't read it
template <class Consumer, class Object>
auto maybeConsume(Consumer&, Object&, long) const -> void
{
}
};
Переводчик берет несколько дополнительных заявителей и просто применяет их последовательно:
template <class... OptionalApplier>
class Translator : public OptionalApplier...
{
public:
// Constructors
Translator(OptionalApplier const&... appliers)
: OptionalApplier(appliers)... {}
Translator(OptionalApplier&&... appliers)
: OptionalApplier(appliers)... {}
// translate fuction
template <class Consumer, class Object>
void translate(Consumer&& consume, Object&& o) const
{
// Apply each optional applier in turn
char _[] = {((void)OptionalApplier::maybeConsume(consume, o), '\0')...};
(void)_;
}
};
Создание функции makeTranslator
теперь действительно просто. Мы просто берем кучу читателей и используем их для создания optionalReader
читателей.
template <class... Reader>
auto makeTranslator(Reader const&... readers)
-> Translator<OptionalReader<Reader>...>
{
return {OptionalReader<Reader>{readers}...};
}
Заключение
Это был длинный пост. Нам нужно было построить много инфраструктуры, чтобы все работало. Однако его действительно просто использовать, и он не требует никаких знаний о том, к каким классам мы его применяем, за исключением тех полей, которые мы хотим использовать.
Мы можем написать переводчиков для многих вещей очень легко!
Пример переводчика изображений
Например, здесь переводчик для изображений и изображений, который также принимает во внимание различные общие названия для таких вещей, как ширина и высота изображения.
Помните, что любой класс изображений, предоставленный переводчику, может при желании реализовать любой из этих методов.
auto getImagesTranslator() {
// Width and height might be implemented as 'getWidth' and 'getHeight',
// Or as 'getRows' and 'getCols'
return makeTranslator(READ_FIELD("width", getWidth()),
READ_FIELD("height", getHeight()),
READ_FIELD("width", getCols()),
READ_FIELD("height", getRows()),
READ_FIELD("location", getLocation()),
READ_FIELD("pixel format", getPixelFormat()),
READ_FIELD("size", size()),
READ_FIELD("aspect ratio", getAspectRatio()),
READ_FIELD("pixel data", getPixelData()),
READ_FIELD("file format", getFileFormat()));
}