Перейти к содержимому

Функции и прохождение параметров

Освоите различные способы передачи аргументов в C++, поймите оптимизацию обратных значений, перегрузку функций и современные атрибуты, такие как [[nodiscard]].

Оригинальное название: The Core Abstraction

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

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

Механизмы прохождения параметров

C++ поддерживает четыре основных способа передачи аргументов. Каждый из них имеет различные компромиссы в отношении производительности, безопасности и семантики:

parameter_passing.cppc++23
#include <iostream>
#include <string>
#include <vector>

// 1. Pass by VALUE — the function gets a copy
//    Use for: small types (int, double, char), when you need a local copy
void square(int x) {           // x is a copy of the argument
    x = x * x;                 // Modifying x does NOT affect the caller
    std::cout << "Inside: " << x << '\n';
}

// 2. Pass by REFERENCE — the function works on the original
//    Use for: when you need to modify the caller's variable
void double_it(int& x) {       // x IS the caller's variable
    x *= 2;                    // This DOES modify the caller's variable
}

// 3. Pass by CONST REFERENCE — read-only access to the original, no copy
//    Use for: large objects you only need to read
void print_all(const std::vector<std::string>& names) {
    for (const auto& name : names) {
        std::cout << name << '\n';
    }
    // names.push_back("Eve");  // ERROR: names is const
}

// 4. Pass by POINTER — explicit nullable indirection
//    Use for: optional parameters (can be nullptr), C API interop
void maybe_increment(int* ptr) {
    if (ptr != nullptr) {       // Must check for null!
        ++(*ptr);
    }
}

int main() {
    int val = 5;

    square(val);
    std::cout << "After square: " << val << '\n';      // Still 5 (copied)

    double_it(val);
    std::cout << "After double_it: " << val << '\n';   // Now 10 (modified)

    std::vector<std::string> people = {"Alice", "Bob", "Charlie"};
    print_all(people);         // No copy of the vector — efficient

    maybe_increment(&val);     // Pass address explicitly
    std::cout << "After maybe_increment: " << val << '\n';  // 11

    maybe_increment(nullptr);  // Safe: the function checks for null
}
Вывод

Нажмите «Запустить», чтобы скомпилировать и выполнить этот пример.

Лучшая практика

Следуйте этим рекомендациям из основных руководящих принципов C++ для выбора параметров прохождения:

Дешево копировать и никогда не изменять? Пройти по значению (int, double, char, bool, небольшие структуры, std::string_view).
Затраты на копирование и чтение? Прохождение const& (std::string, std::vector, крупные объекты).
Необходимо изменить переменную абонента? Прохождение & (неконст-ссылка).
- Может быть недействительным? Проходите по указке. Но вместо этого рассмотрим std::optional&.
Взятие собственности (потоп)? Прохождение по значению и ходу (покрывается модулем семантики хода).

Как правило, объекты размером более 2-3 указателей (16-24 байта на 64-бит) должны передаваться по ссылке, а не по значению.

  • Пройти небольшие, дешевые типы по стоимости: int, double, char, bool, string view
  • Передайте большие объекты только для чтения по ссылке const: const std::string&, const std::vector&
  • Перейти по ссылке non-const только тогда, когда функция должна изменить объект абонента.
  • Перейдите по указателю, когда nullptr является обоснованным аргументом, но предпочтите std::optional, где это возможно.
  • Когда сомневаетесь, проходите мимо ссылки - это безопасно и эффективно

Оптимизация возвратной стоимости (RVO/NRVO)

Общей проблемой для начинающих является то, что возврат больших объектов по стоимости стоит дорого из-за копирования. В современном C++ эта проблема почти всегда необоснована благодаря Оптимизации возвратной стоимости (RVO) и Названной оптимизации возвратной стоимости (NRVO).

RVO (гарантируется с C++17): Когда функция возвращает временный (например, return std::vector{1,2,3};), компилятор должен конструировать объект непосредственно в памяти вызывающего абонента. Ни копирования, ни перемещения не происходит.

NRVO (разрешено, не гарантировано): Когда функция возвращает названную локальную переменную (например, return result;), компилятор может построить эту переменную непосредственно в памяти вызывающего абонента. Все основные компиляторы делают это на практике, даже в режиме отладки.

Практический совет: возврат по стоимости свободно. Не используйте выходные параметры (void f(std::vector& out)), чтобы избежать копий — пусть компилятор оптимизирует возврат.

Функция перегрузки и аргументы по умолчанию

C++ позволяет выполнять несколько функций с одинаковым именем, но с разными списками параметров. Компилятор выбирает лучший матч на основе аргументов, приведенных на сайте вызова:

overloading.cppc++23
#include <iostream>
#include <string>
#include <vector>

// Function overloading — same name, different parameter types/counts
void log(int value) {
    std::cout << "[INT] " << value << '\n';
}

void log(double value) {
    std::cout << "[DOUBLE] " << value << '\n';
}

void log(const std::string& value) {
    std::cout << "[STRING] " << value << '\n';
}

// Default arguments — provide fallback values
void greet(const std::string& name, const std::string& greeting = "Hello") {
    std::cout << greeting << ", " << name << "!\n";
}

// [[nodiscard]] (C++17) — warn if return value is ignored
[[nodiscard]] int compute(int a, int b) {
    return a * b + a;
}

// [[nodiscard]] with reason (C++20)
[[nodiscard("Ignoring error codes can hide failures")]]
int open_file(const std::string& path) {
    // Returns 0 on success, negative on error
    return 0;  // Simplified
}

// inline — suggests the compiler inline this function (also allows
// the function to be defined in a header without ODR violations)
inline int fast_max(int a, int b) {
    return (a > b) ? a : b;
}

int main() {
    log(42);               // Calls log(int)
    log(3.14);             // Calls log(double)
    log(std::string{"hi"});// Calls log(const string&)

    greet("Alice");                // Uses default: "Hello, Alice!"
    greet("Bob", "Good morning"); // Uses provided: "Good morning, Bob!"

    // int result = compute(3, 4);  // OK
    // compute(3, 4);               // WARNING: ignoring [[nodiscard]] return value

    int status = open_file("data.txt");  // OK: return value used
}
Вывод

Нажмите «Запустить», чтобы скомпилировать и выполнить этот пример.

Подводный камень

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

1. Неявные конверсии создают двусмысленность: Если у вас есть f(int) и f(double), вызов f(3.14f) (float) является неоднозначным, потому что float одинаково хорошо конвертируется как в int, так и в double. Исправление: добавьте перегрузку f(float) или используйте static_cast.

2. Аргументы по умолчанию плохо взаимодействуют с перегрузками: void f(int x, int y = 0) и void f(int x) создают двусмысленность при вызове с одним аргументом.

3. const reference vs value: f(int) и f(const int&) двусмысленны для аргументов lvalue, потому что оба одинаково хороши.

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

  • Неявные конверсии могут сделать разрешение перегрузки двусмысленным - добавить явные перегрузки для разрешения
  • Аргументы по умолчанию и перегрузки могут конфликтовать — избегайте обоих на одном и том же имени функции.
  • В случае сомнений сообщение об ошибке компилятора перечисляет перегрузки и двусмысленность кандидата.
  • Используйте static cast для явного выбора перегрузки при вызове с аргументами, которые соответствуют нескольким перегрузкам.
Ключевые выводы
  • Передайте небольшие типы по значению, большие типы только для чтения по ссылке const и изменчивые типы по ссылке non-const
  • Возврат по стоимости свободно — RVO/NRVO устраняет копию на практике
  • Функциональная перегрузка выбирает лучший матч на основе типов аргументов - остерегайтесь двусмысленности от неявных конверсий.
  • Использование [[nodiscard]] на функциях, обратная стоимость которых не должна игнорироваться (коды ошибок, вычисленные результаты)
  • Аргументы по умолчанию обеспечивают удобство, но могут конфликтовать с перегрузками — используйте то или другое.
  • inline позволяет определять функции в заголовках, не нарушая правила One Definition

Тест — проверьте свои знания

(15 XP)

1. Когда вы должны пройти `std::vector<int>` от `const&` вместо стоимости?

2. Что такое оптимизация возвратной стоимости (RVO)?

3. Что делает атрибут `[[nodiscard]]`?