Returning Values from Functions

Getting output from any function is equally important as passing input to it. Like we have different way of passing the parameters similarly we have different way of receiving the output or return values from the function.

In this article we will explore different techniques of returning the values from the function.

Traditional Methods

1 Return by Value

In the traditional approach, a function creates an object locally and returns it by value. This means the object is copied from the function's scope to the caller.

Example:

#include <vector>
#include <iostream>

std::vector<int> createVector() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    return vec;  // A copy is returned (or optimized away by the compiler)
}

int main() {
    std::vector<int> myVector = createVector();
    for (int n : myVector)
        std::cout << n << " ";
    std::cout << "\n";
}

Potential Issue:

  • Copy Overhead: For large objects, copying can be inefficient.

2 Return by Reference or Pointer

To avoid copying, you might return a reference or pointer to an existing object. However, this method requires caution to ensure the referenced object remains valid after the function exits.

Example (Safe Case):

#include <string>
#include <iostream>

std::string globalSetting = "LegacyConfig";

const std::string& getSetting() {
    // Returns a reference to a global variable
    return globalSetting;
}

int main() {
    const std::string& setting = getSetting();
    std::cout << "Setting: " << setting << "\n";
}

Pitfall:

  • Dangling References: Returning a reference or pointer to a local variable is dangerous, as the variable's memory is reclaimed when the function returns.

Modern Techniques

1 Return by Value with RVO and NRVO

Modern C++ compilers often optimize return by value using techniques called Return Value Optimization (RVO) and Named Return Value Optimization (NRVO). These optimizations eliminate unnecessary copying by constructing the object directly in the caller’s memory.

Example:

#include <vector>
#include <iostream>

std::vector<int> createOptimizedVector() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    // Even though it appears we're returning a copy, the compiler can optimize it away.
    return vec;
}

int main() {
    std::vector<int> myVector = createOptimizedVector();
    for (int n : myVector)
        std::cout << n << " ";
    std::cout << "\n";
}

Benefit:

  • Cleaner code without the need to manage object lifetimes manually.
  • Efficient transfers even for large objects.

2 Leveraging Move Semantics

C++11 introduced move semantics, which allow the resources of a temporary object to be "moved" rather than copied. This is especially useful for classes that manage dynamic resources.

Example:

#include <iostream>
#include <vector>

class BigData {
public:
    BigData() { 
        // Simulate a large allocation
        data.resize(1000000, 42);  
    }

    // Move constructor for efficient transfer
    BigData(BigData&& other) noexcept : data(std::move(other.data)) {
        std::cout << "Resources moved!\n";
    }

    // Disable copying to enforce move semantics
    BigData(const BigData&) = delete;
    BigData& operator=(const BigData&) = delete;

private:
    std::vector<int> data;
};

BigData createBigData() {
    BigData data;
    // Process data...
    return data;  // Here, move semantics kick in.
}

int main() {
    BigData myData = createBigData();
    // "Resources moved!" message confirms move constructor usage.
}

Benefits:

  • Efficient handling of large or complex objects.
  • Reduces the overhead associated with copying.

3 Returning Multiple values

Sometimes, a function needs to return more than one value. Modern C++ allows you to bundle multiple values together using std::pair, std::tuple, or even custom structs. With C++17, structured bindings make it even cleaner.

Example using std::tuple:

#include <tuple>
#include <string>
#include <iostream>

std::tuple<int, double, std::string> getMultipleValues() {
    int number = 42;
    double pi = 3.14159;
    std::string greeting = "Hello, World!";
    return std::make_tuple(number, pi, greeting);
}

int main() {
    // Structured bindings (C++17) unpack the tuple into individual variables.
    auto [num, pi, greet] = getMultipleValues();
    std::cout << "Number: " << num << "\n"
              << "Pi: " << pi << "\n"
              << "Greeting: " << greet << "\n";
}

Benefits:

  • Clean and self-documenting code.
  • Avoids using output parameters or global variables.