11.5 – Возвращение значений по значению, по ссылке и по адресу

Добавлено 14 июня 2021 в 02:51

В трех предыдущих уроках вы узнали о передаче аргументов функциям по значению, по ссылке и по адресу. В этом разделе мы рассмотрим задачу возврата значений вызывающему всеми тремя способами.

Как оказалось, возврат вызывающему значений из функции по значению, по адресу или по ссылке работает почти так же, как передача аргументов в функцию. У каждого метода присутствуют все те же плюсы и минусы. Основное различие между ними состоит в том, что направление потока данных меняется на противоположное. Однако есть еще одна дополнительная сложность – поскольку локальные переменные в функции выходят за пределы области видимости и уничтожаются при возврате из функции, нам необходимо учитывать влияние этого на каждый тип возврата.

Возврат по значению

Возврат по значению – это самый простой и безопасный тип возврата значения. Когда значение возвращается по значению, вызывающему возвращается копия этого значения. Как и в случае передачи по значению, вы можете возвращать значения литералов (например, 5), переменных (например, x) или выражений (например, x + 1), что делает возврат по значению очень гибким.

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

int doubleValue(int x)
{
    int value{ x * 2 };
    return value; // Здесь будет возвращена копия value
} // value здесь выходит из области видимости

Возврат по значению является наиболее подходящим при возврате переменных, которые были объявлены внутри функции, или для возврата аргументов функции, переданных по значению. Однако, как и передача по значению, возврат по значению для структур и больших классов выполняется медленно.

Когда использовать возврат по значению:

  • При возврате переменных, которые были объявлены внутри функции
  • При возврате аргументов функции, переданных по значению

Когда не использовать возврат по значению:

  • При возврате встроенного массива или указателя (используйте возврат по адресу)
  • При возврате большой структуры или класса (используйте возврат по ссылке)

Возврат по адресу

Возврат по адресу включает в себя возврат вызывающему адреса переменной. Подобно передаче по адресу, возврат по адресу может возвращать адрес только переменной, но не литерала или выражения (у которых нет адресов). Поскольку возврат по адресу просто копирует адрес из функции вызывающему, он выполняется быстро.

Однако возврат по адресу имеет один дополнительный недостаток, которого нет при возврате по значению – если вы попытаетесь вернуть адрес переменной, локальной для функции, ваша программа будет демонстрировать неопределенное поведение. Рассмотрим следующий пример:

int* doubleValue(int x)
{
    int value{ x * 2 };
    return &value; // здесь возвращаем value по адресу 
} // value здесь уничтожается

Как вы можете видеть здесь, value уничтожается сразу после того, как его адрес возвращается вызывающему. Конечным результатом является то, что вызывающий получает адрес невыделенной памяти (висячий указатель), что вызовет проблемы при использовании. Это частая ошибка, которую допускают начинающие программисты. Многие более новые компиляторы выдают предупреждение (а не ошибку), если программист пытается вернуть локальную переменную по адресу – однако есть довольно много способов обмануть компилятор, чтобы он позволил вам сделать что-то нелегальное без генерации предупреждения, поэтому эта ответственность лежит на программисте, чтобы гарантировать, что указатель, который он возвращает, после возврата из функции будет указывать на допустимую переменную.

Возврат по адресу часто использовался для возврата вызывающему динамически выделенной памяти:

int* allocateArray(int size)
{
    return new int[size];
}
 
int main()
{
    int *array{ allocateArray(25) };
 
    // делаем что-нибудь с массивом
 
    delete[] array;
    return 0;
}

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

Когда использовать возврат по адресу:

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

Когда не использовать возврат по адресу:

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

Возврат по ссылке

Подобно возврату по адресу, значения, возвращаемые по ссылке, должны быть переменными (вы не должны возвращать ссылку на литерал или выражение, которое вычисляется во временное значение, так как они выйдут из области видимости в конце функции, и вы в конечном итоге вернете висячую ссылку). Когда переменная возвращается по ссылке, обратно вызывающему передается ссылка на эту переменную. Затем вызывающий может использовать эту ссылку для продолжения изменения переменной, что иногда может быть полезно. Возврат по ссылке также выполняется быстро, что может быть полезно при возврате структур и классов.

Однако, как и при возврате по адресу, вы не должны возвращать по ссылке локальные переменные. Рассмотрим следующий пример:

int& doubleValue(int x)
{
    int value{ x * 2 };
    return value; // здесь возвращаем ссылку на value
} // здесь value уничтожается

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

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

#include <array>
#include <iostream>
 
// Возвращает ссылку на элемент массива с указанным индексом
int& getElement(std::array<int, 25>& array, int index)
{
    // мы знаем, что array[index] не будет уничтожен,
    // когда мы вернемся к вызывающему (поскольку вызывающий
    // передал массив!)
    // поэтому можно вернуть его по ссылке
    return array[index];
}
 
int main()
{
    std::array<int, 25> array;
 
    // Устанавливаем элемент массива с индексом 10 в значение 5
    getElement(array, 10) = 5;
	
    std::cout << array[10] << '\n';
 
    return 0;
}

Это печатает:

5

Когда мы вызываем getElement(array, 10), getElement() возвращает ссылку на элемент массива с индексом 10. Затем main() использует эту ссылку для присвоения этому элементу значения 5.

Хотя это в некоторой степени надуманный пример (потому что вы можете получить доступ к array[10] напрямую), как только вы узнаете о классах, вы найдете гораздо больше применений для возврата значений по ссылке.

Когда использовать возврат по ссылке:

  • При возврате параметра-ссылки.
  • При возврате члена объекта, который был передан в функцию по ссылке или адресу.
  • При возврате большой структуры или класса, который не будет уничтожен в конце функции (например, тот, который был передан по ссылке).

Когда не использовать возврат по ссылке:

  • При возврате переменных, которые были объявлены внутри функции, или параметров, переданных по значению (используйте возврат по значению).
  • При возврате значения встроенного массива или указателя (используйте возврат по адресу).

Смешивание возвращаемых ссылок и значений

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

int returnByValue()
{
    return 5;
}
 
int& returnByReference()
{
    // static гарантирует, что x не будет уничтожен при завершении функции
    static int x{ 5 }; 
    return x;
}
 
int main()
{
    // случай A - нормально, обрабатывается как возврат по значению
    int giana{ returnByReference() };

    // случай B - ошибка компиляции, поскольку значение является r-значением,
    // а r-значение не может быть привязано к неконстантной ссылке
    int& ref{ returnByValue() }; 

    // случай C - нормально, время жизни возвращаемого значения
    // увеличивается до времени жизни cref
    const int& cref{ returnByValue() };
 
    return 0;
}

В случае A мы присваиваем возвращаемое значение-ссылку переменной-нессылке. Поскольку giana не является ссылкой, возвращаемое значение копируется в giana, как если бы returnByReference() выполняла возврат по значению.

В случае B мы пытаемся инициализировать ссылку ref копией значения, возвращаемого функцией returnByValue(). Однако, поскольку возвращаемое значение не имеет адреса (это r-значение), это вызовет ошибку компиляции.

В случае C мы пытаемся инициализировать константную ссылку cref копией значения, возвращаемого returnByValue(). Поскольку константные ссылки могут связываться с r-значениями, здесь нет проблем. Обычно срок действия r-значения истекает в конце выражения, в котором оно создано, однако, когда оно привязывается к константной ссылке, время жизни r-значения (в данном случае возвращаемого значения функции) увеличивается до времени жизни ссылки (в данном случае cref)

Продление времени жизни не сохраняет висячие ссылки

Рассмотрим следующую программу:

const int& returnByReference()
{
     return 5;
}
 
int main()
{
    const int& ref { returnByReference() }; // ошибка времени выполнения
}

В приведенной выше программе returnByReference() возвращает константную ссылку на значение, которое выйдет за пределы области видимости при завершении функции. Обычно это недопустимо, так как это приведет к висячей ссылке. Однако мы также знаем, что присвоение значения константной ссылке может продлить время жизни этого значения. Итак, что здесь имеет приоритет? Что раньше, 5 выходит из области видимости, или ref продлевает срок жизни 5?

Ответ заключается в том, что сначала 5 выходит за пределы области видимости, затем ссылка на 5 копируется обратно вызывающему, а затем ref продлевает время жизни теперь уже висячей ссылки.

Однако следующий код работает должным образом:

const int returnByValue()
{
     return 5;
}
 
int main()
{
    const int& ref { returnByValue() }; // ok, мы продлеваем время жизни копии,
                                        // переданной обратно в main
}

В этом случае литеральное значение 5 сначала копируется обратно в область видимости вызывающего (main), а затем ref продлевает время жизни этой копии.

Возврат нескольких значений

C++ не содержит прямого метода для передачи нескольких значений обратно вызывающему. Хотя иногда вы можете реструктурировать свой код таким образом, чтобы вы могли передавать каждый элемент данных отдельно (например, вместо того, чтобы одна функция возвращала два значения, использовать две функции, возвращающие по одному из этих значений), но это может быть громоздко и неинтуитивно.

К счастью, есть несколько косвенных методов, которые можно использовать.

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

Второй метод предполагает использование структуры только для данных:

#include <iostream>
 
struct S
{
	int m_x;
	double m_y;
};
 
S returnStruct()
{
	S s;
	s.m_x = 5;
	s.m_y = 6.7;
	return s;
}
 
int main()
{
	S s{ returnStruct() };
	std::cout << s.m_x << ' ' << s.m_y << '\n';
 
	return 0;
}

Третий способ (представленный в C++11) - использовать std::tuple. Кортеж (tuple) – это последовательность элементов, которые могут быть разных типов, где тип каждого элемента должен быть указан явно.

Вот пример, который возвращает кортеж и использует std::get для получения n-го элемента кортежа:

#include <tuple>
#include <iostream>
 
std::tuple<int, double> returnTuple() // вернуть кортеж, содержащий int и double
{
	return { 5, 6.7 };
}
 
int main()
{
	std::tuple s{ returnTuple() }; // получаем наш кортеж

    // используем std::get<n> для получения n-го элемента кортежа
	std::cout << std::get<0>(s) << ' ' << std::get<1>(s) << '\n'; 
 
	return 0;
}

Это работает идентично предыдущему примеру.

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

#include <tuple>
#include <iostream>
 
std::tuple<int, double> returnTuple() // вернуть кортеж, содержащий int и double
{
	return { 5, 6.7 };
}
 
int main()
{
	int a;
	double b;
	std::tie(a, b) = returnTuple(); // помещаем элементы кортежа в переменные a и b
	std::cout << a << ' ' << b << '\n'; 
 
	return 0;
}

Начиная с C++17, для упрощения разделения нескольких возвращаемых значений на отдельные переменные может использоваться объявление структурированной привязки:

#include <tuple>
#include <iostream>
 
std::tuple<int, double> returnTuple() // вернуть кортеж, содержащий int и double
{
	return { 5, 6.7 };
}
 
int main()
{
    // использовали объявление структурированной привязки,
    // чтобы поместить результаты кортежа в переменные a и b
	auto [a, b]{ returnTuple() }; 
	std::cout << a << ' ' << b << '\n';
 
	return 0;
}

Использование структуры – лучший вариант, чем кортеж, если вы используете эту структуру в нескольких местах. Однако для случаев, когда вы просто упаковываете эти значения для возврата, и не будет повторного использования при определении новой структуры, кортеж немного чище, поскольку он не вводит новый пользовательский тип данных.

Заключение

В большинстве случаев возврата по значению будет достаточно. Это также самый гибкий и безопасный способ вернуть информацию вызывающему. Однако возврат по ссылке или адресу также может быть полезен, особенно при работе с динамически размещаемыми классами или структурами. При использовании возврата по ссылке или адресу убедитесь, что вы не возвращаете ссылку или адрес переменной, которая при возврате из функции выйдет за пределы области действия!

Небольшой тест

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

1) Функция с именем sumTo(), которая принимает параметр int и возвращает сумму всех чисел между предыдущими значениями и переданным входным числом.

int sumTo(int value);

2) Функция с именем printEmployeeName(), которая принимает в качестве входных данных структуру Employee.

void printEmployeeName(const Employee& employee);

3) Функция с именем minmax(), которая принимает на вход два числа int и возвращает вызывающему меньшее и большее числа в std::pair. std::pair работает так же, как std::tuple, но хранит ровно два элемента.

std::pair<int, int> minmax(int x, int y);

std::minmax – стандартная функция.

4) Функция с именем getIndexOfLargestValue(), которая принимает массив значений int (как std::vector) и возвращает индекс самого большого элемента в массиве.

std::size_t getIndexOfLargestValue(const std::vector<int>& array);

std::max_element – стандартная функция.

5) Функция с именем getElement(), которая принимает массив из std::string (как std::vector) и индекс и возвращает элемент массива по этому индексу (не копию). Предположим, что индекс действителен, а возвращаемое значение – const.

const std::string& getElement(const std::vector<std::string>& array, std::size_t index);

Теги

C++ / CppLearnCppstd::tupleДля начинающихКортеж / TupleОбучениеПрограммированиеСсылка / Reference (программирование)Указатель / Pointer (программирование)

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.