Перейти к содержанию

💡 Кратко:Все способы вернуть несколько значений из функции в C++ — pair, tuple, структура, tie, коллбэки, выходные параметры — и как std::expected из C++23 выражает «значение-или-ошибка» без исключений.

Последнее обновление:2026-10-24

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

Сегодня зададим ещё один довольно классический вопрос с собеседования: как функция может вернуть несколько результатов? Сколько способов вы можете предложить?

На самом деле этот вопрос проверяет очень много знаний, он затрагивает:

  • указатели
  • ссылки
  • lambda
  • functional
  • tie
  • tuple
  • struct binding
  • template deduce
  • и так далее

В этой статье мы ответим на этот вопрос разными способами. Сначала преобразуем задачу: будем использовать a / b для моделирования (ниже не рассматривается случай деления на ноль) и возвращать частное и остаток. Проще говоря: сколькими способами функция может вернуть частное и остаток?

Переходим к основному тексту. Примечание: весь код этого раздела впервые опубликован в 知识星球; если интересно, отсканируйте QR-код ниже.

1. Указатели и ссылки

Изменим интерфейс, добавив две переменные — с указателями и ссылками.

void divideWithReferences(int dividend, int divisor, int& quotient, int& remainder) {
  quotient = dividend / divisor;
  remainder = dividend % divisor;
}

void divideWithPointers(int dividend, int divisor, int* quotient, int* remainder) {
  if (quotient) *quotient = dividend / divisor;
  if (remainder) *remainder = dividend % divisor;
}

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

2. Tuple + Tie

Сочетание tuple и tie даёт лаконичный способ возвращать из функции несколько значений. Используя std::tie, мы можем распаковать элементы tuple в разные переменные, повышая ясность кода.

Этот метод можно считать одним из эталонных ответов — он довольно простой и наглядный; если возвращаемых значений два, подойдёт и pair.

std::tuple<int, int> divide(int dividend, int divisor) {
    return std::make_tuple(dividend / divisor, dividend % divisor);
}


std::tie(quotient, remainder) = divide(14, 3);
std::cout << quotient << ", " << remainder << std::endl;

3. Struct Binding

Преобразуем предыдущий пример: появившиеся в C++17 структурированные привязки ещё больше упрощают код с возвратом нескольких значений. Использование auto и структурированных привязок делает код более интуитивным и понятным.

Если интервьюер попросит дополнительно усовершенствовать предыдущий метод, можно перейти к приведённому ниже примеру кода: структурированные привязки делают код более элегантным.

auto divide(int dividend, int divisor) {
  struct result {
    int quotient;
    int remainder;
  };
  return result{dividend / divisor, dividend % divisor};
}
auto [quotient, remainder] = divide(14, 3);

4. Callback-функция

В повседневных проектах, особенно в открытых проектах, callback — распространённый приём: передавая callback для обработки возвращаемых значений, мы позволяем пользователю самому определять обработку; так и реализуется возврат нескольких значений и более гибкая структура кода. Это очень полезно для асинхронного программирования и обработки событий.

void divide(int dividend, int divisor, std::function<void(int, int)> callback) {
  callback(dividend / divisor, dividend % divisor);
}

5. Собственный Out

Собственный out — это способ с обёрткой в структуру, при котором выходные параметры становятся членами структуры. Такой подход повышает читаемость кода и особенно уместен для функций, которым нужно возвращать несколько значений.

template <class T>
struct out {
  std::function<void(T)> target;

  out(T* t)
      : target([t](T&& in) {
          if (t) *t = std::move(in);
        }) {}

  template <class... Args>
  void emplace(Args&&... args) {
    target(T(std::forward<Args>(args)...));
  }

  template <class X>
  void operator=(X&& x) {
    emplace(std::forward<X>(x));
  }

  template <class... Args>
  void operator()(Args&&... args) {
    emplace(std::forward<Args>(args)...);
  }
};

void divide(int dividend, int divisor, out<int>& quotient_out, out<int>& remainder_out) {
  quotient_out.emplace(dividend / divisor);
  remainder_out.emplace(dividend % divisor);
}

6. Выведение шаблонов

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

template <typename T1, typename T2>
struct many {
  T1 quotient;
  T2 remainder;
};

template <class T1, class T2>
many(T1, T2) -> many<T1, T2>;

many<int, int> divide(int dividend, int divisor) {
  return many{
      dividend / divisor,
      dividend % divisor,
  };
}

auto [quotient, remainder] = divide(14, 3);

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

C++23 std::expected: значение или ошибка, без исключений

Все приёмы выше возвращают значения — но самое частое «второе возвращаемое значение» в реальном коде это ошибка, и ни один из этих контейнеров не говорит об этом вслух. std::pair компилируется, но ничто не мешает проигнорировать bool. Исключения говорят, но по цене: выворачивают поток управления наизнанку и прячут пути отказов из сигнатуры.

std::expected из C++23 закрывает разрыв. Он хранит либо значение, либо ошибку; к ошибке нельзя обратиться без проверки; монадические помощники выстраивают счастливый путь в цепочку:

#include <charconv>
#include <expected>
#include <string>
#include <string_view>

std::expected<int, std::string> parse_int(std::string_view sv) {
    int value{};
    auto [ptr, ec] = std::from_chars(sv.data(), sv.data() + sv.size(), value);
    if (ec != std::errc{}) {
        return std::unexpected("not a number: " + std::string(sv));
    }
    return value;
}

auto r = parse_int("42");
if (r) { use(*r); } else { log(r.error()); }
// monadic chaining: parse_int(s).and_then(validate).transform(square)

Место среди соседей: std::optional — почти expected, берите его, когда провалу не нужно объяснение; std::variant несёт ту же нагрузку, но без семантики успех/ошибка; expected — версия, объявляющая намерение. Поддержка: GCC 13+, Clang 16+, MSVC начиная с VS 2022 17.3.

Часто задаваемые вопросы

std::expected, std::optional или std::variant — что выбрать?

optional — когда у «нет значения» нет причины. variant — когда обе альтернативы равноправные законные значения. expected — когда второй слот это ошибка, которую вызывающий должен обработать явно.

Выделяет ли std::expected память?

Нет. Как optional и variant, он хранит значение или ошибку прямо в собственном хранилище. Куча не используется; sizeof зависит от T, E и их выравнивания.

Когда всё же стоит использовать исключения вместо std::expected?

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

Похожие темы

Комментарии