[Перевод] Именованные параметры в современном C++
Из Википедии: «Именованные параметры в языках программирования означают поддержку указания явных имен параметров в вызове функции. Вызов функции, принимающей именованные параметры, отличается от обычного вызова функции, в котором передаваемые аргументы ассоциируются с параметрами функции лишь только по их порядку в вызове функции«Давайте посмотрим на пример:
createArray (10, 20); // Что это значит? Что за »10» ? Что за »20» ? createArray (length=10, capacity=20); // О, вот теперь понятнее! createArray (capacity=20, length=10); // И наоборот тоже работает. И еще один пример на выдуманном псевдо-языке:
window = new Window { xPosition = 10, yPosition = 20, width = 100, height = 50 }; Этот подход особенно полезен для функций с большим количеством опциональных параметров, при вызове которых нужно изменить лишь некоторую часть дефолтных значений. Некоторые языки программирования поддерживают именованные параметры (C#, Objective-C, …), но не С++. В этом посте мы рассмотрим пару классических способов эмуляции именованных параметров в С++, ну и попробуем придумать что-то новое.
Комментарии Давайте начнём с ненастоящего, но наиболее простого способа — эмуляция именованных параметров через комментарии :) Window window { 10, // xPosition 20, // yPosition 100, // width 50 // height }; Этот подход весьма популярен среди Windows-разработчиков, поскольку примеры в MSDN часто снабжены такими комментариями.
Идиома «именованного параметра» Идея происходит из стиля программирования на Java: создать прокси-класс, который будет все опциональные параметры включать в себя в виде методов. После этого мы можем использовать цепочку вызовов этих методов для задания только нужных нам параметров: // 1 File f { OpenFile{«path»} // это обязательно .readonly () .createIfNotExist () . … }; // 2 классическая версия (не подходит для случая «хотим оставить всё по-умолчанию») File f = OpenFile { … } .readonly () .createIfNotExist () … ; // 3 для случая «хотим оставить всё по-умолчанию» — просто добавим ещё один слой (вызов CreateFile) auto f = CreateFile (OpenFile («path») .readonly () .createIfNotExists () . …)); Класс OpenFile — это набор параметров, а конструктор File принимает объект этого класса. Некоторые авторы (например, здесь) утверждают, что OpenFile должен иметь только private-члены и объявить класс File дружественным. Это может иметь смысл, если вы хотите использовать какую-то более сложную логику установки параметров. Но для присвоения простых значений вполне пойдет и вышеуказанный стиль с публичными методами.
В этом подходе:
Обязательные параметры всё так-же позиционны (вызов конструктора OpenFile должен быть первым и это нельзя изменить)
Опциональные параметры должны иметь конструкторы копирования (перемещения)
Вам нужно написать дополнительный прокси-класс
Идиома «пакета параметров»
Идея похожа на предыдущую и взята из книги Davide Di Gennaro«s Advanced C++ Metaprogramming — техника использования прокси-объектов для установки параметров через оператор присваивания (=), в итоге мы получим следующий синтаксических сахар:
MyFunction (begin (v), end (v), where[logger=clog][comparator=greater
logger и comparator — глобальные константы. Оператор присваивания просто возвращает обёрнутую копию присваиваемого значения where — глобальная константа типа «пакет параметров». Её оператор [] просто возвращает новый прокси-объект, который заменяет один из своих членов новым аргументом. В символах:
where = {a, b, c } where[logger = x] → { a, b, c }[ argument<0>(x) ] → {x, b, c} Набросок реализации:
// argument
template
{
argument (int = 0)
{
}
template
operator=(const T& that) const
{
return that;
}
argument
operator=(std: ostream& that) const
{
return that;
}
};
// «пакет аргументов» (хранит значения)
template
Хотя техника и кажется интересной, на практике тяжело сделать её достаточно удобной и общной. В книге она вообще была представлена не решением рассматриваемой нами задачи, а примером «цепочного» вызова оператора [].
Теги
Andrzej Krzemieński опубликовал интересный пост «Интуитивный интерфейс», где предложил следующее: именованные параметры представляют собой пары компаньонов — реального значения и пустой структуры (пустые структуры разных типов нужны для выбора нужной перегруженной функции). Вот пример этого подхода из STL:
std: function
// не настоящий STL
std: vector
Кроме того, один из читателей предложил хорошую идею другой реализации тегов.: std: vector v1(std: with_size (10), std: with_value (6));
Boost В Boost есть библиотека параметров.Как и можно было ожидать, это довольно полная и практичная реализация. Пример:
// код класса
#include
Как улучшить читаемость обязательных параметров? Давайте попробуем соединить данный подход с тегами:
auto file = CreateFile (_path, «path», [](auto& r) { r.CreateIfNotExist = true; }); Правда, они всё ещё позиционные. Если вы допускаете возможность получения в рантайме ошибки «обязательный параметр отсутствует» — можно использовать тип optional
Я недавно пробовал использовать данный подход для конфигурации тестов и моков. К примеру, мне нужно было создать тесты для простой игры в кости. Конфигурация и тесты раньше выглядели так:
TEST_F (SomeDiceGameConfig, JustTwoTurnsGame) { GameConfiguration gameConfig { 5u, 6, 2u }; } С использованием данного подхода они могут выглядеть так:
TEST_F (SomeDiceGameConfig, JustTwoTurnsGame) { auto gameConfig = CreateGameConfig ([](auto& r) { r.NumberOfDice = 5u; r.MaxDiceValue = 6; r.NumberOfTurns = 2u; }); } Также мы можем использовать макрос, чтобы не повторяться в каждом тесте с вызовом одинаковых лямбд:
TEST_F (SomeDiceGameConfig, JustTwoTurnsGame) { auto gameConfig = CREATE_CONFIG ( r.NumberOfDice = 5u; r.MaxDiceValue = 6; r.NumberOfTurns = 2u; ); } Использование Variadic Templates Появившиеся в С++11 Variadic Templates могут улучшить способ, описанный выше. Давайте снова вспомним теги. Теги могут быть лучшим подходом, чем лямбда + объект параметров, поскольку нам не нужно создавать ещё один объект, нет проблем с конструкторами копирования, все параметры обрабатываются единообразно (с лямбдами нам приходилось иначе обрабатывать обязательные параметры). Но теги могут быть достаточно хорошим подходом, только если бы у нас вышло: Обойтись объявлением лишь одного перегруженного конструктора или функции Получить возможность свободного определения порядка параметров (пар «тег-значение») Иметь как обязательные, так и опциональные параметры Что-то типа:
File f { _readonly, true, _path, «some path» }; или:
File f { by_name, Args&&… args) {} Моя идея в следующем: я хочу использовать Variadic Templates чтобы дать пользователю возможность определять порядок параметров и опускать опциональные параметры.
Представьте два конструктора:
File (string path, bool readonly, bool createIfNotExist) {} // все параметры обязательны
template
Данная реализация лишь набросок, наверняка её можно улучшить.
Вот как может быть спроектирован класс:
File (string path, bool readonly, bool createIfNotExists /*…*/)
: _path (move (path)), _createIfNotExist (createIfNotExist), _readonly (readonly) // , etc…
{
}
template
auto f = File { by_name, readonly=true, path=«path» }; Основное отличие здесь в передаче аргументов: с прокси мы получаем синтаксический сахар (оператор=), но теперь нам нужно хранить и передавать значения (не очень хорошо для не-перемещаемых/копируемых типов).
Здесь вы можете поэкспериментировать с кодом. Я начал с версии с тегами и потом перешел к прокси, поэтому там обе версии. Вы найдёте две секции под названием «PACK UTILS» (для тегов и прокси).
Вот как будет выглядеть класс:
class window
{
public:
// обычный конструктор
window (string pTitle, int pH, int pW,
int pPosx, int pPosy, int& pHandle)
: title (move (pTitle)), h (pH), w (pW), posx (pPosx), posy (pPosy), handle (pHandle)
{
}
// конструктор, использующий прокси (_title = «title»)
template
Следующий кусок кода показывает, как пользователь может создать объект:
int i=5; // версия с тегами window w1 {use_tags, __title, «Title», __h, 10, __w, 100, __handle, i}; cout << w1 << endl; // версия с прокси window w2 {use_named, _h = 10, _title = "Title", _handle = i, _w = 100}; cout << w2 << endl; // классическая версия window w3 {"Title", 10, 400, 0, 0, i}; cout << w3 << endl; Плюсы:
Обязательные и опциональные параметры используются однообразно Порядок не определён жестко Способ с тегами не имеет недостатков, связанных с передачей параметров Способ с прокси весьма нагляден (за счет оператора =) Минусы:
Ошибки на этапе компиляции могут быть сложны для понимания (static_assert может помочь в некоторых случаях) Доступные параметры должны быть документированы «Загрязнение» пространства имён лишними функциями\конструкторами Значения по-умолчанию всегда вычисляются Способ с тегами не идеален с точки зрения наглядности (тег и значение следуют через запятую) Способ с прокси не идеален с точки зрения передачи параметров Обратите внимание на первую проблему: Clang достаточно умён, чтобы сообщить о проблеме весьма наглядно. Представим, что я забыл об обязательном параметре с названием окна, вот вывод компилятора:
main.cpp:28:2: error: static_assert failed «Required parameter» static_assert (pos >= 0, «Required parameter»); ^ ~~~~~~~~ main.cpp:217:14: note: in instantiation of template class 'get_at<-1, 0>' requested here : window { REQUIRED_NAME (title),
^ Теперь вы достаточно точно знаете, что именно и где было пропущено.Минималистичный подход с использованием std: tuple [этот параграф написал Davide Di Gennaro]Мы можем использовать функционал кортежей (std: tuple) для написания весьма компактной и портируемой реализации нашей задачи. Мы будем опираться на несколько простых принципов:
Набор параметров будет специальным кортежем, где после каждого «типа тега» будет идти его значение (то есть тип будет чем-то вроде (std: tuple
Внутри вызова функции клиент извлечёт нужные значения из набора параметров и как-то их использует (ну например запишет в локальные переменные):
namespace tag
{
CREATE_TAG (age, int);
CREATE_TAG (name, std: string);
}
template
Сначала макрос:
#include
struct age_t
{
std: tuple
age = 18 Преобразовывается во что-то типа:
make_tuple (parameter
std: tuple
int myage = 18; f (myage); // ok g ((…) + (age=18)); // ok g ((…) + (age=myage)); // ошибка компиляции, а также избыточно с точки зрения читабельности Кроме того, мы можем использовать семантику перемещения:
Разница между
std: tuple
std: tuple
В виде альтернативы мы могли бы написать:
std: tuple
Мы неявно соглашаемся с тем, что все кортежи, начинающиеся с parameter были созданы нашим кодом, так что без всякой явной валидации мы просто выбросим parameter.
template
tuple
Ну и наконец, мы напишем функцию извлечения аргумента из набора. Обратите внимание, что данная функция имеет семантику переноса (т.е. после её вызова параметр будет извлечён из набора).
template
Чтобы сделать этот выбор возможным, функция будет выглядеть как:
template
extract_from_pack< erorr_policy > (age, myage, mypack);
В виду правил работы с variadic templates, extract_from_pack знает, что набор параметров имеет форму tuple
extract_from_pack< erorr_policy > (age, myage, mypack); вызывает
extractor<0, erorr_policy >:: extract (age, myage, mypack); который далее вызывает
extractor<0, erorr_policy >:: extract (age, myage, std: get<0>(pack), mypack); который имеет два перегруженных варианта:
extract (TAG, … , TAG, …) которые, если выполняется, выполняет присваивание и возвращает true или
extract (TAG, … , DIFFERENT_TAG, …) который продолжает итерацию, вызывая снова
extractor<2, erorr_policy >:: extract (age, myage, mypack); когда продолжение итерации невозможно — вызывается error_policy: err (…)
template
struct soft_error
{
template
struct hard_error
{
template
(age=18)+(age=19)
Финальные заметки
Мы не обсудили рантайм-техники, вроде:
void MyFunction (option_parser& pack)
{
auto name = pack.require («name»).as
А ещё я нашел предложение добавить именованные параметры в стандарт языка С++ вот здесь. Неплохо было бы.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.