💡 Resumo:Todas as formas de retornar múltiplos valores em C++ — pair, tuple, struct, tie, callbacks, parâmetros de saída — e como o std::expected do C++23 expressa retornos de valor-ou-erro sem exceções.
Última atualização:2026-10-24
Formas de retornar múltiplos valores em C++¶
Hoje trago mais uma pergunta clássica de entrevista: como uma função pode retornar vários resultados? Quantos métodos você consegue imaginar?
Na verdade, essa questão avalia muitos pontos de conhecimento, envolvendo:
- Ponteiros
- Referências
- lambda
- functional
- tie
- tuple
- struct binding
- template deduce
- entre outros
Este artigo responderá à pergunta usando métodos diferentes. Primeiro, reformulemos o problema: use a / b para simular (abaixo, sem considerar o caso de divisor 0) e retorne quociente e resto. Em resumo: de quantas maneiras uma função pode retornar o quociente e o resto?
Vamos ao conteúdo principal. Nota: todos os códigos desta seção foram publicados primeiro no Planeta do Conhecimento; se tiver interesse, escaneie o QR code abaixo.
1. Ponteiros e referências¶
Altere a interface, adicionando duas variáveis, usando ponteiros e referências.
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;
}
As desvantagens dessa abordagem são evidentes: quando há mais de dois valores de retorno, a interface fica muito longa. Além disso, se o entrevistador exigir que a interface da função não seja alterada, esse método não poderá ser usado.
2. Tuple + Tie¶
A combinação de tuple e tie oferece uma forma concisa de uma função retornar vários valores. Ao usar std::tie, podemos desempacotar os elementos do tuple em variáveis diferentes, melhorando a clareza do código.
Esse método pode ser considerado uma das respostas padrão: é simples e claro; se os valores retornados forem dois, um pair também serve.
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¶
Refinando o anterior, o structured binding introduzido no C++17 simplifica ainda mais o código de retorno de múltiplos valores. Usando auto e structured binding, o código fica mais intuitivo e fácil de entender.
Se o entrevistador pedir para você melhorar ainda mais o método acima, ele pode se tornar o exemplo de código a seguir, usando structured binding para deixar o código mais elegante.
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 de função¶
Em projetos do dia a dia, especialmente em projetos de código aberto, o callback é um recurso muito usado: passando um callback que processa os valores retornados, o usuário define o processamento como quiser, realizando assim o retorno de múltiplos valores e tornando a estrutura do código mais flexível. Isso é muito útil em cenários como programação assíncrona e tratamento de eventos.
void divide(int dividend, int divisor, std::function<void(int, int)> callback) {
callback(dividend / divisor, dividend % divisor);
}
5. Out personalizado¶
O out personalizado é uma abordagem que envolve os parâmetros de saída como membros de uma estrutura. Esse método melhora a legibilidade do código e é especialmente adequado para funções que precisam retornar vários valores.
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. Dedução de templates¶
A dedução de templates do C++ oferece aos desenvolvedores uma forma mais flexível e concisa de escrever código. Com ela, podemos tratar dados de tipos diferentes sem especificar os tipos explicitamente.
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);
De modo geral, o C++ oferece várias formas de implementar o retorno de múltiplos valores, cada uma com seus cenários de aplicação. A escolha adequada depende das necessidades concretas; os desenvolvedores podem usar essas características com flexibilidade, conforme a estrutura e a legibilidade do código.
C++23 std::expected: valor ou erro, sem exceções¶
Todas as técnicas acima retornam valores — mas o "segundo valor de retorno" mais comum em código real é um erro, e nenhum desses contêineres diz isso. std::pair
O std::expected
#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)
Onde ele fica entre os vizinhos: std::optional
Perguntas frequentes¶
std::expected, std::optional ou std::variant — qual usar?¶
optional quando o "sem valor" não precisa de motivo. variant quando as duas alternativas são valores legítimos de igual categoria. expected quando a segunda casa é um erro que você quer que o chamador trate explicitamente.
O std::expected aloca memória?¶
Não. Como optional e variant, ele armazena o valor ou o erro inline no próprio armazenamento. Não há alocação no heap; sizeof depende de T, E e do alinhamento.
Quando devo usar exceções em vez de std::expected?¶
Mantenha exceções para condições realmente excepcionais — falhas de construção, erros que atravessam muitas camadas — onde o stack unwinding compensa. Use expected para falhas esperadas e recuperáveis perto do ponto de chamada, como parsing e validação.
Tópicos relacionados¶
- Funções virtuais puras e classes abstratas em C++ | C++那些事
- Asserções com assert em C++ | C++那些事
- Bit fields em C++ | C++那些事
- Implementando polimorfismo de C++ em linguagem C | C++那些事
- const em C++ explicado: constantes, ponteiro const vs ponteiro para const | C++那些事
- A família completa do const em C++ | C++那些事
- O operador de desreferência * em C++ | C++那些事
- Inicialização e atribuição em C++ | C++那些事
- A palavra-chave mutable em C++ | C++那些事
- C++ restrict e otimização do compilador | C++那些事