c ++ 11 — Лучшие практики + синтаксис для реализации & quot; contextmanager & quot; в переполнении стека

Наша кодовая база Python имеет код, связанный с метриками, который выглядит следующим образом:

class Timer:
def __enter__(self, name):
self.name = name
self.start = time.time()

def __exit__(self):
elapsed = time.time() - self.start
log.info('%s took %f seconds' % (self.name, elapsed))

...

with Timer('foo'):
do some work

with Timer('bar') as named_timer:
do some work
named_timer.some_mutative_method()
do some more work

В терминологии Python таймер contextmanager.

Теперь мы хотим реализовать то же самое в C ++ с одинаково хорошим синтаксисом. К сожалению, C ++ не имеет with, Таким образом, «очевидная» идиома будет (классический RAII)

class Timer {
Timer(std::string name) : name_(std::move(name)) {}
~Timer() { /* ... */ }
};

if (true) {
Timer t("foo");
do some work
}
if (true) {
Timer named_timer("bar");
do some work
named_timer.some_mutative_method();
do some more work
}

Но это чрезвычайно уродливая синтаксическая соль: она на много строк длиннее, чем нужно, нам пришлось ввести имя t для нашего «неназванного» таймера (и код молча ломается, если мы забываем это имя) … это просто безобразно.

Каковы некоторые синтаксические идиомы, которые люди использовали для работы с «контекстными менеджерами» в C ++?


Я думал об этой оскорбительной идее, которая уменьшает количество строк, но не избавляет от имени t:

// give Timer an implicit always-true conversion to bool
if (auto t = Timer("foo")) {
do some work
}

Или это архитектурное чудовище, которому я даже не доверяю себя правильно использовать:

Timer("foo", [&](auto&) {
do some work
});
Timer("bar", [&](auto& named_timer) {
do some work
named_timer.some_mutative_method();
do some more work
});

где конструктор Timer на самом деле вызывает данную лямбду (с аргументом *this) и делает запись всего за один раз.

Однако ни одна из этих идей не кажется «лучшей практикой». Помоги мне здесь!


Другой способ сформулировать вопрос может быть следующим: если вы проектировали std::lock_guard с нуля, как бы вы сделали это, чтобы устранить как можно больше шаблонов? lock_guard Это прекрасный пример контекстного менеджера: это утилита, по сути RAII, и вам вряд ли захочется ее называть.

5

Решение

Вам не нужно if( true )В C ++ есть «анонимные области», которые можно использовать для ограничения времени жизни области практически так же, как в Python with или C # s using (ну, в C # тоже есть анонимные области).

Вот так:

doSomething();
{
Time timer("foo");
doSomethingElse();
}
doMoreStuff();

Просто используйте голые фигурные скобки.

Тем не менее, я не согласен с вашей идеей использования семантики RAII для инструментов кода, как это в качестве timer Деструктор нетривиален и имеет побочные эффекты. Это может быть уродливым и повторяющимся, но я чувствую, явно называя по имени startTimer, stopTimer а также printTimer Методы делают программу более «правильной» и самодокументируемой. Побочные эффекты плохие, ладно?

1

Другие решения

Изменить: После более внимательного прочтения комментария Дая и немного подумав, я понял, что это плохой выбор для C ++ RAII. Зачем? Поскольку вы входите в деструктор, это означает, что вы делаете io, и io может бросить. Деструкторы C ++ не должны создавать исключения. С питоном пишем метание __exit__ это не обязательно круто, это может привести к тому, что вы бросите свое первое исключение на пол. Но в python вы точно знаете, вызвал ли код в менеджере контекста исключение или нет. Если это вызвало исключение, вы можете просто пропустить вход __exit__ и пройти через исключение. Я оставляю свой оригинальный ответ ниже, если у вас есть менеджер контекста, который не рискует бросить при выходе.

Версия C ++ на 2 строки длиннее версии Python, по одной на каждую фигурную скобку. Если C ++ только на две строки длиннее, чем python, это хорошо. Диспетчеры контекста предназначены для этой конкретной вещи, RAII является более общим и предоставляет строгий расширенный набор функций. Если вы хотите ознакомиться с передовым опытом, вы уже нашли его: создайте анонимную область и создайте объект в начале. Это идиоматично. Это может показаться вам некрасивым из Python, но в мире C ++ это просто прекрасно. Точно так же кто-то из C ++ сочтет менеджеры контекста безобразными в определенных ситуациях Я использую оба языка профессионально, и это меня совсем не беспокоит.

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

template <class F>
void with_timer(const std::string & name, F && f) {
Timer timer(name);
f();
}

Использование:

with_timer("hello", [&] {
do something;
});

Это эквивалентно анонимному диспетчеру контекста в том смысле, что ни один из методов Таймера не может быть вызван, кроме конструирования и уничтожения. Кроме того, он использует «нормальный» класс, так что вы можете использовать этот класс, когда вам нужен именованный менеджер контекста, и эту функцию в противном случае. Очевидно, вы можете написать with_lock_guard очень похожим образом. Там это даже лучше, так как у lock_guard нет функций-членов, в которых вы упускаете возможность.

Все сказанное, буду ли я использовать with_lock_guard или одобрить код, написанный товарищем по команде, который добавлен в такую ​​утилиту? Нет. Одна или две дополнительные строки кода просто не имеют значения; эта функция не добавляет достаточно полезности, чтобы оправдать свое существование. YMMV.

1

По вопросам рекламы ammmcru@yandex.ru
Adblock
detector