Scaling Return Structures of C++ Functions Systematically

Viewed 108

I have a couple functions that return not just one variable, but a collection of variables. For example, a root finder might return the found root, iterations needed, and whether the root-finding process was indeed successful.

struct ReturnData { 
    const double root;
    const size_t iterations;
    const bool success;
};

template <class FuncType>
ReturnData findRoot(const FuncType& f, const double& guess) {
    double root = guess;
    size_t iterations = 0;

    while(true) { /*... find root ...*/ }

    return { root, iterations, true };
}

Now this works if I am only returning a small number of variables. But maybe I want to include a possible error message like const std::string errorMessage in my ReturnData, as well as other reference meta data. So when scaling this to, say, more than 10 parameters, it starts to be very easy to make mistakes in using the initializer list to return the data. Maybe I also deleted a parameter, or I re-ordered them, etc. which in turn requires me to carefully apply the same changes to the initializer list at the end of function findRoot.

What is a good systematic approach to return data from a function like this? My criteria are:

  1. Clear code that's straight-forward to understand/maintain by other people.
  2. Somewhat efficient code, meaning that I don't want to construct overly elaborate return structures (since this root-finder might be called many many times, and in the majority of cases people will only care about variable root ).
4 Answers

You should probably just create a variable and set its members:

    //...

    ReturnData rd; 
    rd.root        = root;
    rd.iterations  = iterations;
    rd.success     = true;

    return rd;
};

I wouldn't worry about performance since there is RVO.

If you are overly concerned about performance you could declare a variable of ReturnData at the begin of your function and use its members:

template <typename FuncType>
ReturnData findRoot(FuncType const &f, double const &guess) {
    ReturnData rd;

    while( /* ... */ ) { /*...*/ rt.root = /*...*/; ++rd.iterations; }

    rd.success = true;
    return rd;
};

When you have worries that the return value is not properly initialized, add a constructor that makes sure every necessary argument is passed to it. As the constructor now handles class invariants, the data members should not be public anymore, and you need getter methods. If this sounds overly complicated, you need to live with the risk that a return value could be improperly constructed. Or - keep the data members public and make them const. You loose the ability to assign to instances of this type, but this could be acceptable if you only initialize them upon invoking findRoot.

In terms of scaling, any solution will be opinionated, hence only my two cents: don't hesitate to add further structs and include them as data members in ReturnData. Any group of parameters that can be grouped in a sensible way justifies a new type. Whenever information that the function returns should be optional, well, use std::optional. This is exactly what the type is about.

And finally, as you mentioned efficiency in the question as well - when you think there is a bottleneck, measure first. It's unlikely that ReturnData will slow down an application. Not only because return value optimziation, also because findRoot must be quite complex to gather so much data to return that the function itself will probably dominate any performance analysis.

In case you're targeting gcc and/or clang, you can use designated initializers:

return {
    .root = root,
    .iterations = iterations,
    .success = true
};

This was originally introduced in C99 and is planned for C++2a. Both gcc and clang support it as an extension, unfortunately MSVC doesn't.

Alternative approach could be using structures, as follows:

struct Root { double value; };
struct Iterations { size_t value; };
struct Success { bool value; };

struct ReturnData 
{
    const Root root;
    const Iterations iterations;
    const Success success;
};

// usage:
return {
    Root { root },
    Iterations { iterations },
    Success { true }
}; 

One way is to give the values unique types.
It's slightly tedious, but it works.
You can use a template to cut down on the tediousity.

Something along these lines:

template<typename T, typename>
struct NewType
{
    T value;
};

using Root = NewType<double, struct Root_T>;
using Success = NewType<bool, struct Success_T>;

struct ReturnData { 
    const Root root;
    const Success success;
};

template <class FuncType>
ReturnData findRoot(const FuncType& f, const double& guess) {
    double root = guess; 
    while(true) { /*... find root ...*/ }
    return { Root{root}, Success{true} }
}
Related