Для возврата нескольких «выходных» значений предпочитайте возврат структуры
Причина
Возвращаемое значение само документирует себя как «только выходное» значение. Заметьте, что C++ поддерживает множественные возвращаемые значения по соглашению об использовании tuple-подобных типов (struct, array, tuple и т.д.), возможно, с дополнительным удобством структурированных привязок (C++17) на стороне вызова. Предпочитайте именованный struct, когда это возможно. В ином случае tuple полезен в вариадических шаблонах.
Пример
// ПЛОХО: параметр только для вывода, задокументированный в комментарии
int f(const string& input, /*только вывод*/ string& output_data)
{
// ...
output_data = something();
return status;
}
// ХОРОШО: самодокументируемый
struct f_result { int status; string data; };
f_result f(const string& input)
{
// ...
return {status, something()};
}
Стандартная библиотека C++98 использовала этот стиль в некоторых местах, возвращая pair в ряде функций. Например, для set<string> my_set:
// C++98
pair<set::iterator, bool> result = my_set.insert("Hello");
if (result.second)
do_something_with(result.first); // обходной приём
В C++17 мы можем использовать «структурированные привязки», чтобы дать каждому члену имя:
if (auto [ iter, success ] = my_set.insert("Hello"); success)
do_something_with(iter);
Структура с осмысленными именами более распространена в современном C++. Смотрите, например, ranges::min_max_result, from_chars_result и другие.
Исключение
Иногда нужно передать объект в функцию для манипуляции его состоянием. В таких случаях передача объекта по ссылке T& обычно является правильной техникой. Явная передача параметра вход-выход обратно как возвращаемого значения часто не нужна. Например:
istream& operator>>(istream& in, string& s); // похоже на std::operator>>()
for (string s; in >> s; ) {
// сделать что-то со строкой
}
Здесь и s, и in используются как параметры вход-выход. Мы передаём in по ссылке на не-const, чтобы иметь возможность манипулировать его состоянием. Мы передаём s, чтобы избежать повторных выделений памяти. Повторно используя s (переданный по ссылке), мы выделяем новую память только когда нужно расширить ёмкость s. Эта техника иногда называется паттерном «caller-allocated out» и особенно полезна для типов, таких как string и vector, которым нужно выделять память в свободном хранилище.
Для сравнения, если бы мы передавали все значения как возвращаемые, писали бы примерно так:
struct get_string_result { istream& in; string s; };
get_string_result get_string(istream& in) // не рекомендуется
{
string s;
in >> s;
return { in, move(s) };
}
for (auto [in, s] = get_string(cin); in; s = get_string(in).s) {
// сделать что-то со строкой
}
Мы считаем это значительно менее элегантным и значительно менее производительным.
Для строго буквального прочтения этого правила (F.21) исключение на самом деле не является исключением, поскольку оно опирается на параметры вход-выход, а не на простые выходные параметры, упомянутые в правиле. Однако мы предпочитаем быть явными, а не тонкими.
Примечание
В большинстве случаев полезно возвращать конкретный, пользовательский тип. Например:
struct Distance {
int value;
int unit = 1; // 1 означает метры
};
Distance d1 = measure(obj1); // доступ к d1.value и d1.unit
auto d2 = measure(obj2); // доступ к d2.value и d2.unit
auto [value, unit] = measure(obj3); // доступ к value и unit; несколько избыточно
// для тех, кто знает measure()
auto [x, y] = measure(obj4); // не делайте так; это, вероятно, запутает
Чрезмерно обобщённые pair и tuple следует использовать только когда возвращаемое значение представляет независимые сущности, а не абстракцию.
Другой вариант — использовать optional<T> или expected<T, error_code>, вместо pair или tuple. При правильном использовании эти типы передают больше информации о смысле членов, чем pair<T, bool> или pair<T, error_code>.
Примечание
Когда возвращаемый объект инициализируется из локальных переменных, дорогих для копирования, явный move может быть полезен для предотвращения копирования:
pair<LargeObject, LargeObject> f(const string& input)
{
LargeObject large1 = g(input);
LargeObject large2 = h(input);
// ...
return { move(large1), move(large2) }; // без копирования
}
Альтернативно:
pair<LargeObject, LargeObject> f(const string& input)
{
// ...
return { g(input), h(input) }; // без копирования, без перемещений
}
Заметьте, что это отличается от антипаттерна return move(...) из ES.56.
Контроль
Выходной параметр — это тот, в который функция записывает, вызывает не-const функцию-член или передаёт как не-const.
В вариадических шаблонах tuple часто неизбежен.
- Выходные параметры должны быть заменены возвращаемыми значениями.
- Возвращаемые типы
pairилиtupleдолжны быть замененыstruct, если возможно.