Именованные параметры в современном C++

Автор оригинала: Marco Arena, Davide Di Gennaro
  • Перевод
  • Tutorial
Из Википедии: "Именованные параметры в языках программирования означают поддержку указания явных имен параметров в вызове функции. Вызов функции, принимающей именованные параметры, отличается от обычного вызова функции, в котором передаваемые аргументы ассоциируются с параметрами функции лишь только по их порядку в вызове функции"

Давайте посмотрим на пример:

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<int>()]);


Задействованные сущности:
  • logger и comparator — глобальные константы. Оператор присваивания просто возвращает обёрнутую копию присваиваемого значения
  • where — глобальная константа типа «пакет параметров». Её оператор [] просто возвращает новый прокси-объект, который заменяет один из своих членов новым аргументом.


В символах:

where = {a, b, c }
where[logger = x] → { a,b,c }[ argument<0>(x) ]  →   {x,b,c}


Набросок реализации:

// argument
template <size_t CODE, typename T = void>
struct argument
{
   T arg;
   argument(const T& that)
      : arg(that)
   {
   }
};
 
// void argument - just to use operator=
template <size_t CODE>
struct argument<CODE, void>
{
   argument(int = 0)
   {
   }
   template <typename T>
   argument<CODE, T> operator=(const T& that) const
   {
     return that;
   }
   argument<CODE, std::ostream&> operator=(std::ostream& that) const
   {
      return that;
   }
};
 
// "пакет аргументов" (хранит значения)
template <typename T1, typename T2, typename T3>
struct argument_pack
{
   T1 first;
   T2 second;
   T3 third;
   argument_pack(int = 0)
   {
   }
   argument_pack(T1 a1, T2 a2, T3 a3)
     : first(a1), second(a2), third(a3)
   {
   }
   template <typename T>
   argument_pack<T, T2, T3> operator[](const argument<0, T>& x) const
   {
      return argument_pack<T, T2, T3>(x.arg, second, third);
   }
   template <typename T>
   argument_pack<T1, T, T3> operator[](const argument<1, T>& x) const
   {
      return argument_pack<T1, T, T3>(first, x.arg, third);
   }
   template <typename T>
   argument_pack<T1, T2, T> operator[](const argument<2, T>& x) const
   {
      return argument_pack<T1, T2, T>(first, second, x.arg);
   }
};
 
enum { LESS, LOGGER };
const argument<LESS> comparator = 0;
const argument<LOGGER> logger = 0;
typedef argument_pack<basic_comparator, less<int>, std::ostream> pack_t;
static const pack_t where(basic_comparator(), less<int>(), std::cout);


За полным кодом загляните в оригинальную книгу.

Хотя техника и кажется интересной, на практике тяжело сделать её достаточно удобной и общной. В книге она вообще была представлена не решением рассматриваемой нами задачи, а примером «цепочного» вызова оператора [].

Теги


Andrzej Krzemieński опубликовал интересный пост «Интуитивный интерфейс», где предложил следующее: именованные параметры представляют собой пары компаньонов — реального значения и пустой структуры (пустые структуры разных типов нужны для выбора нужной перегруженной функции). Вот пример этого подхода из STL:

std::function<void()> f{std::allocator_arg, a}; // a - аллокатор
std::unique_lock<std::mutex> l{m, std::defer_lock}; // отложенный lock


Andrzej предложил обобщить подход:

// не настоящий STL
std::vector<int> v1(std::with_size, 10, std::with_value, 6);


Как вы понимаете, потребуется создать некоторое количество перегруженных функций, а также вы не можете выбирать порядок параметров. К плюсам можно отнести отсутствие необходимости наличия конструкторов копирования\переноса. Передача значений по-умолчанию также работает без проблем. Из статьи: «Теги не являются идеальным решением, поскольку засоряют пространства имён перегруженными функциями, которые полезны лишь в нескольких местах их вызова»

Кроме того, один из читателей предложил хорошую идею другой реализации тегов.:
std::vector v1(std::with_size(10), std::with_value(6));

Boost


В Boost есть библиотека параметров.

Как и можно было ожидать, это довольно полная и практичная реализация. Пример:

// код класса
#include <boost/parameter/name.hpp>
#include <boost/parameter/preprocessor.hpp>
#include <string>
 
BOOST_PARAMETER_NAME(foo)
BOOST_PARAMETER_NAME(bar)
BOOST_PARAMETER_NAME(baz)
BOOST_PARAMETER_NAME(bonk)
 
BOOST_PARAMETER_FUNCTION(
   (int), // возвращаемый тип функции
   function_with_named_parameters, // имя функции
   tag, // часть "магии". Если вы используете BOOST_PARAMETER_NAME, в этом месте нужно вставить "tag"
   (required // имена и типы всех обязательных параметров
      (foo, (int))
      (bar, (float))
   )
   (optional // имена, типы и значения по-умолчанию всех опциональных параметров
      (baz, (bool) , false)
      (bonk, (std::string), "default value")
   )
)
{
   if (baz && (bar > 1.0)) return foo;
      return bonk.size();
}
 
// код клиента
function_with_named_parameters(1, 10.0);
function_with_named_parameters(7, _bar = 3.14);
function_with_named_parameters( _bar = 0.0, _foo = 42);
function_with_named_parameters( _bar = 2.5, _bonk= "Hello", _foo = 9);
function_with_named_parameters(9, 2.5, true, "Hello");


Именованные параметры в современном С++


Последние стандарты языка С++ открывают новые двери. Давайте посмотрим, получится ли применить что-нибудь из них для решения нашей задачи.

Лямбды


Метод «цепочных вызовов» слишком многословен. Я не хочу добавлять кучу функций, возвращающих сам объект. Как на счёт определить структуру и устанавливать её члены через лямбда-функции?

struct FileRecipe
{
   string Path; // обязательный параметр
   bool ReadOnly = true; // опциональный параметр
   bool CreateIfNotExist = false; // опциональный параметр
   // ...
};
 
class File
 {
   File(string _path, bool _readOnly, bool _createIfNotexist)
      : path(move(_path)), readOnly(_readOnly), createIfNotExist(_createIfNotExist)
 {}
 
private:
   string path;
   bool readOnly;
   bool createIfNotExist;
 };
 
auto file =  CreateFile( "path", [](auto& r) { // такая-себе мини-фабрика
   r.CreateIfNotExist = true;
});


Нам всё ещё нужен класс для хранения параметров, но сам подход масштабируется лучше, чем классическая идиома именованного параметра, в которой нужно явно прописать все «цепочные» функции. Ещё один вариант — сделать конструктор класса File, принимающий объект типа FileRecipe.

Как улучшить читаемость обязательных параметров? Давайте попробуем соединить данный подход с тегами:

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<typename... Args>
File(by_name_t, Args&&... args) {}


Объект типа File может быть создан любым из двух способов. Если вы используете второй конструктор — он просмотрит все параметры в наборе и вызовет первый конструктор с соответствующим набором параметров. Просмотр параметров и генерация кода выполняется на этапе компиляции, занимает линейное время и не влияет на затраты времени на вызов в рантайме.

Данная реализация лишь набросок, наверняка её можно улучшить.

Вот как может быть спроектирован класс:

File(string path, bool readonly, bool createIfNotExists /*...*/)
   : _path (move(path)), _createIfNotExist(createIfNotExist), _readonly(readonly) // ,etc...
{
}
 
template<typename Args...>
File(named_tag, Args&&... args)
   : File{ REQUIRED(path), OPTIONAL(read, false) // , etc... } // делегирование
{
}


Перед тем как показать вам работающий код, давайте проясним, что ту же самую идею мы можем применить к прокси:

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<typename... pack>
    window(use_named_t, pack&&... _pack)
       : window { REQUIRED_NAME(title), // required
                  OPTIONAL_NAME(h, 100), // optional
                  OPTIONAL_NAME(w, 400), // optional
                  OPTIONAL_NAME(posx, 0), // optional
                  OPTIONAL_NAME(posy, 0), // optional
                  REQUIRED_NAME(handle) } // required
    {
    }
 
    // конструктор, использующий теги (__title, "title")
    template<typename... pack>
    window(use_tags_t, pack&&... _pack)
       : window { REQUIRED_TAG(title), // required
                  OPTIONAL_TAG(h, 100), // optional
                  OPTIONAL_TAG(w, 400), // optional
                  OPTIONAL_TAG(posx, 0), // optional
                  OPTIONAL_TAG(posy, 0), // optional
                  REQUIRED_TAG(handle) } // required
    {
    }
 
private:
  string title;
  int h, w;
  int posx, posy;
  int& handle;
};


Как вы видите, оба последних конструктора всегда вызывают «классический» конструктор для выполнения реальной работы.

Следующий кусок кода показывает, как пользователь может создать объект:

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<age_tag, int, name_tag, string, … >)
  • Стандартная библиотека языка уже включает функции передачи / конкатенации объектов и кортежей, что гарантирует производительность и корректность
  • Мы будем использовать макрос для определения глобальных констант, представляющих тег
  • Синтаксис создания набора параметров будет выглядеть как (tag1=value1)+(tag2=value2)+…
  • Клиент будет принимать набор параметров как ссылку на шаблонный тип, т.е.

    template <typename pack_t>
    void MyFunction([whatever], T& parameter_pack) // или const T&, T&&, и т.д.
  • Внутри вызова функции клиент извлечёт нужные значения из набора параметров и как-то их использует (ну например запишет в локальные переменные):


namespace tag
{
   CREATE_TAG(age, int);
   CREATE_TAG(name, std::string);
}
 
template <typename pack_t>
void MyFunction(T& parameter_pack)
{
   int myage;
   std::string myname;
   bool b1 = extract_from_pack(tag::name, myname, parameter_pack);
   bool b2 = extract_from_pack(tag::age, myage, parameter_pack);
   assert(b1 && myname == "John");
   assert(b2 && myage == 18);
}
 
int main()
{
   auto pack =  (tag::age=18)+(tag::name="John");
   MyFunction(pack);
}


Вот как может выглядеть реализация этой идеи.

Сначала макрос:

#include <tuple>
#include <utility>
 
template <typename T>
struct parameter {};
 
#define CREATE_TAG(name, TYPE) \
\
   struct name##_t \
   { \
      std::tuple<parameter<name##_t>, TYPE> operator=(TYPE&& x) const \
      {  return std::forward_as_tuple(parameter<name##_t>(), x); } \
      \
      name##_t(int) {} \
}; \
\
const name##_t name = 0


Раскрытие макроса CREATE_TAG(age, int) создаёт класс и глобальный объект.

struct age_t
{
   std::tuple<parameter<age_t>, int> operator=(int&& x) const
   {
      return std::forward_as_tuple(parameter<age_t>(), x);
   }
   age_t(int) {}
};
 
const age_t age = 0;



Концептуально присваивание

age = 18


Преобразовывается во что-то типа:

make_tuple(parameter<age_t>(), 18);


Обратите внимание, что мы написали:

std::tuple<parameter<age_t>, int> operator=(int&& x) const


Мы требуем r-value справа. Это сделано ради безопасности: ради повышения читабельности кода с наборами параметров вы можете захотеть присваивать константы, а не переменные.

int myage = 18;
f(myage); // ok
 
g((...) + (age=18)); // ok
g((...) + (age=myage)); // ошибка компиляции, а также избыточно с точки зрения читабельности 


Кроме того, мы можем использовать семантику перемещения:

Разница между
std::tuple<parameter<age_t>, int> operator=(int&& x) const
{
   return std::make_tuple(parameter<age_t>(), x);
}


и

std::tuple<parameter<age_t>, int> operator=(int&& x) const
{
   return std::forward_as_tuple(parameter<age_t>(), x);
}


очень тонкая. В последнем случае возвращается std::tuple<…, int&&>, но поскольку функция возвращает std::tuple<…, int> — вызывается конструктор перемещения std::tuple.

В виде альтернативы мы могли бы написать:

std::tuple<parameter<age_t>, int> operator=(int&& x) const
{
   return std::make_tuple(parameter<age_t>(), std::move(x));
}


А теперь мы напишем подходящий оператор конкатенации для наших кортежей.

Мы неявно соглашаемся с тем, что все кортежи, начинающиеся с parameter были созданы нашим кодом, так что без всякой явной валидации мы просто выбросим parameter.

template <typename TAG1, typename... P1, typename TAG2, typename... P2>
std::tuple<parameter<TAG1>, P1..., parameter<TAG2>, P2...>
operator+ (std::tuple<parameter<TAG1>, P1...>&& pack1, std::tuple<parameter<TAG2>, P2...>&& pack2)
{
    return std::tuple_cat(pack1, pack2);
}


Очень простая функция: проверяет, что оба кортежа имеют вид

tuple<parameter<tag>, type, [maybe something else]>


и соединяет их.

Ну и наконец, мы напишем функцию извлечения аргумента из набора. Обратите внимание, что данная функция имеет семантику переноса (т.е. после её вызова параметр будет извлечён из набора).

template <typename TAG, typename T, typename... P, typename TAG1>
bool extract_from_pack(TAG tag, T& var, std::tuple<parameter<TAG1>, P...>& pack);


Работает она следующим образом: если набора содержит parameter, тогда переменная получает значение, следующее непосредственно за ним и функция возвращает true. Иначе случается что-то плохое (мы можем выбрать — ошибка компиляции, вернуть false, сгенерировать исключение).

Чтобы сделать этот выбор возможным, функция будет выглядеть как:

template <typename ERR, typename TAG, typename T, typename... P, typename TAG1>
bool extract_from_pack(TAG tag, T& var, std::tuple<parameter<TAG1>, P...>& pack)


и вызывать мы её будем вот так:
extract_from_pack< erorr_policy > (age, myage, mypack);


В виду правил работы с variadic templates, extract_from_pack знает, что набор параметров имеет форму tuple<parameter, … >, так что нужно проверить рекурсивно действительно ли TAG равен TAG1. Мы реализуем это направлением вызова классу:

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 <size_t N, typename ERR>
struct extractor
{
   template <typename USERTAG, typename T, typename TAG, typename... P>
   static bool extract(USERTAG tag, T& var, std::tuple<parameter<TAG>, P...>&& pack)
   {
      return extract(tag, var, std::get<N>(pack), std::move(pack));
   }
 
   template <typename USERTAG, typename T, typename TAG, typename... P>
   static bool extract(USERTAG tag, T& var, parameter<TAG> p0, std::tuple<P...>&& pack)
   {
      return extractor<(N+2 >= sizeof...(P)) ? size_t(-1) : N+2, ERR>::extract(tag, var, std::move(pack));
   }
 
   template <typename USERTAG, typename T, typename... P>
   static bool extract(USERTAG tag, T& var, parameter<USERTAG>, std::tuple<P...>&& pack)
   {
      var = std::move(std::get<N+1>(pack));
      return true;
   }
};
 
template <typename ERR>
struct extractor<size_t(-1), ERR>
{
   template <typename TAG, typename T, typename DIFFERENT_TAG, typename... P>
   static bool extract(TAG tag, T& var, std::tuple<parameter<DIFFERENT_TAG>, P...>&& pack)
   { return ERR::err(tag); }
};
 
template <typename ERR, typename TAG, typename T, typename... P, typename TAG1>
bool extract_from_pack(TAG tag, T& var, std::tuple<parameter<TAG1>, P...>& pack)
{
   return extractor<0, ERR>::extract(tag, var, std::move(pack));
}


В виду гибкой природы наборов параметров, лучшей политикой обработки ошибком может считаться “return false” (любое более строгое поведение будет на самом деле означать обязательность каждого параметра).

struct soft_error
{
   template <typename T>
   static bool err(T)
   {
      return false;
   }
};


Тем ни менее, если зачем-то нужно, мы можем выбрать также из вот этих двух:

struct hard_error
{
   template <typename T>
   static bool err(T); // обратите внимание, что static_assert(false) здесь не работает. Можете ли вы догадаться почему?
};
 
struct throw_exception
{
   template <typename T>
   static bool err(T)
   {
      throw T();
      return false;
   }
};


Дополнительным усовершенствованием может быть проверка избыточности для таких случаев как:
(age=18)+(age=19)


Финальные заметки


Мы не обсудили рантайм-техники, вроде:

void MyFunction (option_parser& pack)
{
   auto name = pack.require("name").as<string>();
   auto age = pack.optional("age", []{ return 10; }).as<int>();
   ...
}


Код работает на рантайме, пытаясь достать нужные ему параметры по ходу работы, соответственно мы имеем затраты времени, ну и об ошибке вы узнаете лишь когда она возникнет. Код далёк от идеала, я привожу его лишь как «proof of concept» и не думаю, что в таком виде его можно применять в реальных проектах.

А ещё я нашел предложение добавить именованные параметры в стандарт языка С++ вот здесь. Неплохо было бы.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Нужны ли в С++ именованные параметры на уровне языка?

  • 45,4%Конечно, удобно же!161
  • 25,1%Ну, мне не особо важно, но пусть будут89
  • 29,6%Хватит ломать язык и добавлять всякую чушь!105
Инфопульс Украина
Creating Value, Delivering Excellence

Похожие публикации

Реклама
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее

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

    +11
    Сколько всего понапридумывали люди, нет чтобы просто добавить именованные параметры в язык и не мучаться! Я в каком-то давнем обсуждении даже синтаксис предлагал — unary dot notation.
    void foo(int x=1, int y=2, int z=3) {}
    foo(.y=100, .z=200, .x = 300);
    foo(.z=500);
    


    Вообще это печальная сторона С++. С одной стороны, программисты реально хотят появления новых языковых возможностей — отсюда появляются и активно используются библиотеки типа Boost. С другой стороны, новые возможности в сам язык вводить не спешат… костыли из Буста становятся все более распространенными, а затем их начинают затаскивать в стандарт, они попадают в стандарт, и получается, что вроде как и возможность реализована — но каким-то немыслимо кривым и вывернутым способом. Вместо добавления небольшого количества простого и понятного кода в компилятор добавляют огромное количество непонятного кода на шаблонах в библиотеку.
      +1
      Давайте посмотрим на огромное количество непонятного кода на шаблонах, добавленного в стандартную библиотеку C++11:
      Threading
      Tuple types
      Hash tables
      Regular expressions
      Smart pointers
      Random number
      Wrapper reference
      Polymorphic wrappers for function objects
      Type traits for metaprogramming

      Что из этого вы бы предложили заменить простым и понятным кодом в компиляторе?
        0
        Главная фишка С++ — огромный спектр применений. С++ используется везде — от нижнего уровня взаимодействия с железом до разработки UI.
        Не всем это понятно, но все «языковые возможности» кто-то где-то должен запрограммировать, и оно потом должно быть слинковано с вашей программой.
        Таки образом внедрение в язык «возможностей» — обрезает эти применения до UI, т.к. делает обязательной и раздутой стандартную библиотеку.
        0
        Threading частично (как в C#),
        Regular expressions (со статичной компиляцией константных рв во время компиляции программы),
        Smart pointers,
        Wrapper reference
        PS. Промахнулся, ответ OlegMax
          +3
          Не представляю, что может так не устраивать в нынешней реализации smart pointers, чтобы потребовался перенос в синтаксис.
          0
          (ответ olegmax )
          Все что называется «language feature emulation», многое из «metaprogramming» и «generic programming».
          Из вашего списка пожалуй type traits, может быть polymorphic wrappers for function objects.
          Tuples однозначно, их нужно на уровне синтаксиса слить со списками инициализации. Тогда можно писать выражения вида {i, j, k}=10; или {i, j, k} = {10,20,30}, множественные возвраты из функий — и все красивым и естественным путем.
          Еще:
          Any, Optional, Variant — должна быть прямая языковая поддержка; для опционального типа взять "?" из C#.
          Все что касается эмуляции концептов.
          Вообще шаблоны нужно расширить так, чтобы они плавно переходили в синтаксические макросы, я пожалуй в новогодние каникулы статейку напишу по этому поводу.
          Полноценные функциональные типы должны быть встроены в язык и иметь синтаксис вида (char,float)=>int
          Сигналы и слоты скорее всего должны быть встроены в язык на уровне синтаксиса с реализацией по умолчанию, но с возможностью переопределять реализацию
          Сопрограммы безусловно на уровне синтаксиса в язык
          Ну и т.д, я всего буста не знаю к сожалению, но там несомненно найдется еще много кандидатов.

          Разумеется, в виде библиотек должны остаться такие вещи как математика, файловая система, регулярные выражения, случайные числа… То есть общая идея — в язык вносится все то, для чего нужна работа с AST, типизатором, кодогенератором. Все, что не касается языка как такового — остается в виде библиотек.
            0
            Во-первых, я отвечал строго про «затаскивать в стандарт», как это сейчас происходит — выглядит все не так катастрофично. Видение развития C++ — отдельная тема.
            Во-вторых, сейчас можно писать return std::make_tuple{10, 20, 30} или std::tie{i, j, k}, что, конечно, ужас-ужас-ужас по сравнению с предложенными вами вариантами. Но, с другой стороны, нам — плюсистам за это доплачивают.
              0
              Tuples, Optional, Variant
              Нужно добавить нормальные алгебраические типы и pattern mathing.
              Сомнительные вещи из C# лучше не брать, если речь о том, чтобы просто использовать T? как синоним std::optional, то это наверное нормально, но эту противоестественную семантику, когда одно и тоже выражение может иметь и тип T и T? не надо.

              boost::variant сейчас это вообще тихий ужас.
              –10
              >>>createArray(10, 20); // Что это значит? Что за «10»? Что за «20»?

              Вот именно. Что это за хардкод? Что за мэджик намберс? Может лучше не допускать такого в коде и все будет ОК?
              А вы хотите «легализовать» такой хардкод… Замазываете симптомы, а не лечите болезнь.
                0
                Речь не о хардкоде «10» и «20», а о том, что же означают первый и второй параметры функции.
                  0
                  Дело не в магических числах, а в порядке следования параметров. Ну назовете вы переменные «capacity» и «length», только как это поможет понять, что из этих двух методов надо вызвать:
                  createArray(capacity, length);
                  createArray(length, capacity);
                  

                  Для сравнения: вот так выглядел бы вызов подобной функции в Objective-C:
                  [self createArrayWithLength:length andCapacity:capacity];
                  
                  +3
                  Именованные параметры не могут быть добавлены на уровень языка, без включения имен параметров в сигнатуру функции (сейчас в сигнатуре только типы и порядок параметров), что сильно ломает обратную совместимость, которая является одним из главных критериев для дальнейшего развития стандарта.
                    +2
                    Конечно в N4172 предложен вариант без утери обратной совместимости, но использование такого варианта может принести вместо удобства головную боль: например автор используемой третьесторонней библиотеки решит переименовать параметры, или вообще убрать их имена — имеет полное право. А про переносимость таких программ можно будет вообще забыть.
                      +1
                      Именованные параметры не нужно включать в сигнатуру. Значения по умолчанию не включены в сигнатуру, и имена аргументов не нужно.
                        –1
                        Допустим:
                        void func(int x=0);
                        

                        А теперь попробуем:
                        template<class F, class ... Args>
                        auto call(F f, Args&& ... args) {
                            return f(std::forward<Args>(args)...);
                        }
                        
                        ...
                        
                        call(func);
                        

                        Или даже:
                        auto f = func;
                        f();
                        


                        Значения по умолчанию практически ничем не отличаются от макросов, и всего-лишь делают замену на уровне препроцессора с:
                        func();
                        

                        на:
                        func(0);
                        


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

                        Неужели c++ недостаточно твердо стоит на двух костылях макросов и значений по умолчанию, что его еще нужно подпереть третьим?
                          +1
                          Я что-то не пойму, Вы предлагаете сделать так, чтобы:
                          typedef int (*reductionFunc)(int accumulator, int value);
                          
                          int myReductionFunc(int acc, int v) {
                            return acc + v;
                          }
                          
                          reductionFunc f = myReductionFunc;
                          

                          давало type error из-за несоответствия имён аргументов? Или зачем тогда имена аргументов в сигнатуре?
                            0
                            Именно. Но я этого не предлагаю, напротив, говорю, что такой вариант невозможен, а без него именованные параметры превратятся в дополнительную препроцессорную магию, когда имена вроде как есть и используются, но вроде как и нет, так как средствами языка невозможно узнать о их существовании или отсутствии (например при помощи sfinae). Какая-то квантовая механика, а не программирование.
                              +1
                              Во-первых, не очень понятно, где и зачем может понадобиться ориентироваться на имена аргументов при помощи SFINAE (а это то ещё костылище само по себе). Всё равно, первичной в C++ остаётся позиция аргумента, а не его имя. И так останется до тех пор, пока кардинально не изменится calling convention. Т.е. всё равно нельзя будет использовать f(int x, y) вместо g(int y, x) через, например, указатель на функцию, даже с именованными аргументами, чтобы x попадал в x, а y попадал в y в обеих функциях.

                              Во-вторых, хотя и можно смотреть на реализацию значений по-умолчанию как на«практически ничем не отличающуюся от макросов», однако вредных побочных эффектов макросов всё-таки нет. А отсутствие возможности использовать в SFINAE — это всё-таки отсутствие чего-то (чего и так нет), а не наличие чего-то вредного.

                              В-третьих, пусть такая реализация неполна, не решает 100% проблем и т.д., но хотя бы сколько-то удобства кому-то даёт, не добавляя никаких существенных вредных эффектов.

                              Более того, можно было бы разрешить делать что-нибудь вроде (имея в виду пример из моего предыдущего комментария):
                              int reduce(std::vector<int> vec, reductionFunc f) {
                                int acc = 0;
                                for (int v : vec) {
                                  acc = f(accumulator=acc, value=v);
                                }
                                return acc;
                              }
                              ...
                              
                              int r = reduce(myVec, myReductionFunc);
                              

                              т.е. разрешить использовать имена аргументов из объявления типа, а не из объявления функции. Правда, сейчас GCC позволяет объявлять типы указателей на функции со значениями по умолчанию, но не позволяет эти значения использовать. В стандарт не заглядывал, поэтому не знаю, это по стандарту нельзя, не обязательно или просто ещё не доделали.
                                0
                                Правда, сейчас GCC позволяет объявлять типы указателей на функции со значениями по умолчанию, но не позволяет эти значения использовать.
                                А можно пример, как это выглядит?
                                  +1
                                  Да, видимо перепутал с чем-то. Объявлять тоже нельзя. А жаль :)
                            –1
                            Проблема вашего кода в том, что f — это указатель на функцию, а не сама функция. Поэтому, если у вас есть скажем вторая функция:
                            void other_func(int arg = 10);
                            
                            то и call(func), и call(other_func) вызывают одну и ту же функцию
                            void call<void (*)(int)>( void (*f)(int) )
                            

                            Логично и то, что при сохранении указателя на функцию теряется информацию о параметрах по умолчанию, иначе вы либо не смогли бы сохранить в один указатель вашу func и функцию без параметра по умолчанию, например:
                            void yet_another_func(int arg);
                            
                            либо указатель на функцию пришлось бы делать некой нетривиальной структурой данных, которая при вызове функции выполняет нетривиальный код с возможным броском исключения в случае, если сохраненная в указателе функция аргумент по умолчанию не поддерживает.

                            Чтобы ваш код заработал, надо чтобы для func и other_func вызывались разные версии шаблонной функций. Этого можно достичь, например, завернув каждую функцию в свой функциональный объект:
                            call([](auto&&... args){ func(std::forward<decltype(args)>(args)...); });
                            call([](auto&&... args){ other_func(std::forward<decltype(args)>(args)...); });
                            


                            Правда, как нетрудно заметить, этот код не совсем эквивалентен предыдущему, он работает не с отдельной функцией, а со всем семейством перегруженных функций с одинаковым именем. Работать с одной конкретной функцией с поддержкой аргументов по умолчанию, вроде как, нельзя, возможно, тут есть некоторая недоработка.
                              –1
                              Проблема в том, что значение по умолчанию Шредингера находится в суперпозиции — оно одновременно есть, когда мы пытаемся вызвать функцию, опуская его; и его нет, когда мы пытаемся наблюдать его.
                              Чтобы ваш код заработал, надо чтобы для func и other_func вызывались разные версии шаблонной функций. Этого можно достичь, например, завернув каждую функцию в свой функциональный объект:
                              call([](auto&&... args){ func(std::forward<decltype(args)>(args)...); });
                              call([](auto&&... args){ other_func(std::forward<decltype(args)>(args)...); });
                              


                              А можно просто отказаться от плохой практики использования значения по умолчанию в пользу перегрузок и вызова функций согласно их сигнатурам:
                              void func(int x);
                              void func() {
                                  func(0);
                              }
                              
                              ...
                              
                              call(static_cast<void(&)()>(func)); // static_cast необходим для выбора перегруженного варианта
                              call(static_cast<void(&)(int)>(func),10);
                              
                                0
                                от плохой практики использования значения по умолчанию
                                Честно говоря, так и не понял, чем эта практика плоха. Можно поподробней? Указатель на функцию, принимающую меньшее количество параметров, легко получить простой оберткой:
                                void func(int x = 0) {}
                                void (*ptr)() = [](){ func(); };
                                

                                Ваш способ же вынуждает нас поддерживать согласованными между собой несколько функций, кроме того это всё будет некрасиво выглядеть в IDE: вместо одной сигнатуры функции с подсказками значений аргументов по умолчанию мы получим несколько функций без всяких подсказок.
                        0
                        Довольно часто попадаются статьи о С++, где рассказывается не о том, как реализовать что-то интересное, а том, как с помощью каких-то ужасных извращений сделать вполне обычные вещи.
                        В новых стандартах стараются улучшить ситуацию, получается зачастую не особо, видимо язык уже изначально плохо спроектирован.
                        Он как PHP, появился в нужное время, в нужном месте и многие на него подсели, вместо того, чтобы посидеть, хорошенько подумать и сделать что-нибудь получше.
                          +3
                          Интересно а чем не угодил довольно старый но по моему мнению очень удобный подход через структуры, когда много параметров?
                          OpenDesc FileDesc;
                          FileDesc.Path = "../file.file";
                          FileDesc.OpenFor = READ;
                          FileDesc.CreateIfNotExist = true;
                          ...
                          auto lv_File = OpenFile(FileDesc);
                          

                          К примеру такое сделали для DirectX10,11 и по моему это очень удобно.
                            +1
                            Ну, если поставить цель докопаться на ровном месте, то можно попенять многословностью (вон сколько раз FileDesc повторяется — в каждой строке), ну и передачей по значению (хотя легко заменить на ссылку). А вообще я согласен — вполне хороший вариант сейчас, сам пользуюсь.
                              +1
                              Вот тут бы не помешала фича c99: designated initializer
                              struct Point {
                                int x;
                                int y;
                                int z;
                              };
                              
                              struct Point p = {
                                .y = 1,
                                .x = 2
                              };
                              


                              В контексте бы C++11 могло бы получиться что-то вроде:
                              struct Point {
                                int x = 0;
                                int y = 0;
                                int z = 0;
                              };
                              
                              void some_proc(const Point &p) { ... }
                              
                              some_proc({
                                .x = 12,
                                .z = 22
                              });
                              


                              Со своими ограничениями, но просто получилась бы фишка без глубоких модификаций спеков и компиляторов (просто адаптировать код C99 компилятора).
                            +11
                            Через такие костыли как представлены в статье эта фича нафиг не нужна. Тем более что именованные параметры просто синтаксический сахар, а не жизненно необходимая вещь.
                              0
                              По-моему, кто-то просто использует неполноценную IDE, отсюда и проблемы.
                              image
                                0
                                Единстенное, после NetBeans-а такую подсказку уж никак нельзя назвать «полноценной», скорее, ущербной. Как и в MSVS.
                                  0
                                  А после KDevelop все ущербны
                                    +3
                                    В контексте данной статьи на скриншоте присутствует вся необходимая информация: порядок параметров, их имена и тип.
                                    Но все же хотелось бы увидеть пример подсказки в NetBeans (ставить ради этого лень, так что если не затруднит — будьте добры).

                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                Самое читаемое