[Перевод] Очистить код конечного автомата в C++

C++

Очистить код конечного автомата в C++

Это гостевой пост Валентина Толмера. Валетин — инженер-программист в Google, пытающийся улучшить качество окружающего его кода. В молодом возрасте на него повлияло программирование шаблонов, и сейчас он занимается только метапрограммированием. ты сможешьGitHubНайдите некоторые из его работ, особенно те, которые описаны в этой статье.ProtEncбиблиотека.

Вы когда-нибудь сталкивались с такой аннотацией?

// 重要:在调用 SetUp() 之前请不要调用该函数!

Или сделайте такую ​​проверку:

if  (my_field_.empty())  abort();

Эти требования (требования проверки состояния, упомянутые в комментариях) являются общими для протоколов, которым должен соответствовать наш код. Бывают случаи, когда явный протокол, которому вы соответствуете, также требует проверки состояния, например, при квитировании SSL или другой реализации бизнес-логики. Или, может быть, у вас есть конечный автомат в вашем коде с явными переходами состояний, который должен каждый раз выполнять проверки состояния перехода по списку возможных переходов.

Давайте посмотрим, как мы можемчеткосправиться с этим сценарием.

Например: установить HTTP-соединение

Наш сегодняшний пример — построение HTTP-соединения. Для значительного упрощения скажем так, что наш запрос на подключение содержит как минимум один заголовок (может быть и больше), и одно и только одно тело, и эти заголовки должны быть указаны перед телом (например, из соображений производительности мы пишем только добавление структура данных).

Примечания: Хотя этоконкретныйПроблему можно решить, передав конструктору правильные параметры, я не хочу усложнять этот протокол. Вы увидите, как легко его расширить.

Это первая реализация:

class  HttpConnectionBuilder  {
 public:
  void  add_header(std::string  header)  {
    headers_.emplace_back(std::move(header);
  }
  // 重要: 至少调用一次 add_header 之后才能被调用
  void  add_body(std::string  body)  {
    body_  =  std::move(body);
  }
  // 重要: 只能调用 add_body 之后才能被调用
  // 消费对象
  HttpConnection build()  &&  {
    return  {std::move(headers_),  std::move(body_)};
  }
 private:
  std::vector<std::string>  headers_;
  std::string  body_;
};

До сих пор пример был довольно простым, но он полагался на то, что пользователь не делает ничего плохого: если он не прочитал документацию заранее, ничто не мешало ему добавить еще один заголовок после тела. Если вы поместите это в файл из 1000 строк, вы быстро увидите, насколько это плохо. Что еще хуже, нет никакой проверки того, что класс используется правильно, поэтому единственный способ увидеть, используется ли класс неправильно, — это увидеть, есть ли непреднамеренные эффекты! Если это вызвало повреждение памяти, удачи в отладке.

На самом деле, мы можем сделать лучше...

Использовать динамическое перечисление

При нормальных обстоятельствах соглашение может представлять собой конечный автомат для представления: конечный автомат запускается в состоянии, в котором мы не добавляли никаких заголовков (состояние START), только возможность добавить заголовок этого состояния. Затем переходит к добавлению по крайней мере одного заголовка (состояние HEADER), это состояние может либо добавить дополнительный заголовок для хранения состояния, может быть добавлено в тело и в состояние BODY. Только в этом состоянии мы можем вызвать BODY build, переходим к конечному состоянию.

typestates state machine

Итак, давайте запишем эти идеи в наш класс!

enum  BuilderState  {
  START,
  HEADER,
  BODY
};
class  HttpConnectionBuilder  {
  void  add_header(std::string  header)  {
    assert(state_  ==  START  ||  state_  ==  HEADER);
    headers_.emplace_back(std::move(header));
    state_  =  HEADER;
  }
  ...
 private:
  BuilderState state_;
  ...
};

То же самое касается и других функций. Это уже хорошо: у нас есть некое состояние, говорящее нам, какой переход возможен, и мы его проверяем. Конечно, у вас есть хорошо продуманные тестовые примеры для вашего кода, верно? Если ваши тесты имеют достаточное покрытие кода, вы сможете отловить любые неправильные операции во время тестирования. Вы также можете включить эти проверки в рабочей среде, чтобы убедиться, что вы не отклоняетесь от этого протокола (контролируемые сбои всегда лучше, чем повреждение памяти), но вы должны платить за дополнительные проверки.

Использование состояний типов

Как мы можем отловить эти ошибки быстрее и со 100% точностью? Тогда пусть компилятор сделает всю работу! Ниже я представлю понятие состояний типов.

Грубо говоря, состояния типов — это типы, которые кодируют состояние объекта в его собственный тип. Некоторые языки делают это, реализуя отдельный класс для каждого состояния (например,HttpBuilderWithoutHeader,HttpBuilderWithBodyи т. д.), но в C++ это было бы очень многословно: нам пришлось бы объявлять конструкторы, удалять функции копирования, преобразовывать один объект в другой... и срок его действия скоро истечет.

Но у C++ есть еще одна крутая штука: шаблоны! мы можемenumзакодировать состояние в и использовать этоenumШаблон конструктора. В результате получается следующий код:

template  <BuilderState  state>
class  HttpConnectionBuilder  {
  HttpConnectionBuilder<HEADER> 
  add_header(std::string  header)  &&  {
    static_assert(state  ==  START  ||  state  ==  HEADER, 
      "add_header can only be called from START or HEADER state");
    headers_.emplace_back(std::move(header));
    return  {std::move(*this)};
  }
  ...
};

Здесь мы статически проверяем, что объект находится в правильном состоянии, неверный код даже не компилируется! И мы также можем получить довольно четкое сообщение об ошибке. Каждый раз, когда мы создаем новый объект, соответствующий целевому состоянию, мы также уничтожаем объект, соответствующий предыдущему состоянию: вы находитесь в типеHttpConnectionBuilder<START>вызовите add_header для объекта, но вы получитеHttpConnectionBuilder<HEADER>Тип возвращаемого значения. Это основная идея типов состояний.

Примечание. Этот метод можно вызывать только для значений r (std::move, который находится в конце строки объявления функции&&эффект). Почему должно быть так?它强制性地破坏了前一个状态,因此只能得到一个相关的状态。可以将其看做unique_ptr: вы не хотите дублировать внутренний виджет и получить недопустимое состояние. какunique_ptrПоскольку существует только один владелец, состояния типов также должны иметь только одно состояние.

При этом вы можете написать:

auto connection  =  GetConnectionBuilder()
  .add_header("first header")
  .add_header("second header")
  .add_body("body")
  .build();

Любое отклонение от протокола приведет к сбою компиляции.

Вот несколько правил, которым нужно следовать несмотря ни на что:

  • Все ваши функции должны использовать ссылки rvalue на объекты (такие как*thisДолжна быть ссылка на rvalue, должна быть в конце&&).
  • Возможно, вам придется отключить функции копирования, если нет смысла переходить к промежуточному состоянию протокола (вот почему у нас все-таки есть ссылки на rvalue).
  • Вы должны объявить свой конструктор закрытым и добавить фабричную функцию, чтобы люди не создавали объект без начального состояния.
  • Нужно добавить конструктор перемещения в друзья и реализовать в другое состояние, без которого можно перемещать объекты из одного состояния в другое по желанию.
  • Вам нужно убедиться, что вы добавили проверки в каждую функцию.

В целом, правильная реализация этих с нуля - это немного сложно, и естественный рост, вы, вероятно, не хотите, чтобы 15 различных типов реализации домашнего состояния (типистики). Если есть рамки, которые можно легко и безопасно объявить эти типы состояний, как!

Библиотека ProtEnc

ЭтоProtEnc(сокращение от кодировщик протокола), где он вступает в игру. Благодаря поразительному количеству шаблонов библиотека позволяет легко объявлять классы, реализующие проверку состояния типа. Для его использования требуется ваша (непроверенная) реализация протокола, которая является первым классом, который мы реализуем со всеми «важными» аннотациями.

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

Итак, как нам объявить этот класс-оболочку? Во-первых, мы должны определить конечный автомат. Он состоит из трех частей: начального состояния, перехода и конечного состояния или перехода. Список начальных состояний — это просто список наших типов перечислений, например:

using  MyInitialStates  =  InitialStates<START>;

Для переходов нам нужно состояние инициализации, конечное состояние и функция, которая выполняет переход состояния:

using  MyTransitions  =  Transitions<
  Transition<START,  HEADERS,  &HttpConnectionBuilder::add_header>,
  Transition<HEADERS,  HEADERS,  &HttpConnectionBuilder::add_header>,
  Transition<HEADERS,  BODY,  &HttpConnectionBuilder::add_body>>;

Для финального перехода нам также понадобится состояние и функция:

using  MyFinalTransitions  =  FinalTransitions<
  FinalTransition<BODY,  &HttpConnectionBuilder::build>>;

Эти дополнительные «FinalTransitions» связаны с тем, что мы можем определить несколько «FinalTransition».

Теперь мы можем объявить тип нашего класса-оболочки. Некоторые неизбежные шаблоны скрыты определениями макросов, но в основном это конструкции базового класса или метаобъявления.

PROTENC\_DECLARE\_WRAPPER(HttpConnectionBuilderWrapper,  HttpConnectionBuilder,  BuilderState,  MyInitialStates,  MyTransitions,  MyFinalTransitions);

Это расширенная область (класс), куда мы можем перенаправить нашу функцию:

PROTENC\_DECLARE\_TRANSITION(add_header);
PROTENC\_DECLARE\_TRANSITION(add_body);
PROTENC\_DECLARE\_FINAL_TRANSITION(build);

Затем он закрывает область.

PROTENC\_END\_WRAPPER;

(Это всего лишь закрывающая скобка, но вы же не хотите, чтобы скобки не совпадали, не так ли?)

通过这个简单但可扩展的设置,你就可以像使用上一步中的包装器一样使用它啦,并且所有的操作都会被检查。 🙂

auto connection  =  HttpConnectionBuilderWrapper<START>{}
  .add_header("first header")
  .add_header("second header")
  .add_body("body")
  .build();

Попытка вызвать функцию в порядке ошибки приведет к ошибкам компиляции. Не волнуйтесь, тщательно продуманный дизайн гарантирует, что первое сообщение об ошибке будет читабельным. Например, удалить.add_body("body")строку, вы получите следующую ошибку:

In file included from example/http_connection.cc:6:

src/protenc.h:  In  instantiation of  ‘struct  prot_enc::internal::return\_of\_final\_transition\_t<prot_enc::internal::NotFound,  HTTPConnectionBuilder>’:
src/protenc.h:273:15:     required by  ...
example/http_connection.cc:174:42:     required from here
src/protenc.h:257:17:  error:  static  assertion failed:  Final  transition not  found
   static_assert(!std::is\_same\_v<T,  NotFound>,  "Final transition not found");

Просто убедитесь, что класс-оболочка может быть построен только из оболочки, и вы сможете гарантировать правильное функционирование всей кодовой базы!

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

Полный пример кода можно найти по адресуGitHubоказаться. Обратите внимание, что этот код не может использовать Clang сейчас, потому чтоbug #35655.

Вам также понравится

Если вы обнаружите ошибки в переводе или в других областях, требующих доработки, добро пожаловать наПрограмма перевода самородковВы также можете получить соответствующие бонусные баллы за доработку перевода и PR. начало статьиПостоянная ссылка на эту статьюЭто ссылка MarkDown этой статьи на GitHub.


Программа перевода самородковэто сообщество, которое переводит высококачественные технические статьи из Интернета сНаггетсДелитесь статьями на английском языке на . Охват контентаAndroid,iOS,внешний интерфейс,задняя часть,блокчейн,продукт,дизайн,искусственный интеллектЕсли вы хотите видеть более качественные переводы, пожалуйста, продолжайте обращать вниманиеПрограмма перевода самородков,официальный Вейбо,Знай колонку.