Search code examples
c++api-designrvalue-reference

Is there a way to make a function have different behavior if its return value will be used as an rvalue reference instead of an lvalue?


I have a routine that does some moderately expensive operations, and the client could consume the result as either a string, integer, or a number of other data types. I have a public data type that is a wrapper around an internal data type. My public class looks something like this:

class Result {
 public:
  static Result compute(/* args */) {
    Result result;
    result.fData = new ExpensiveInternalObject(/* args */);
    return result;
  }

  // ... constructors, destructor, assignment operators ...

  std::string toString() const { return fData->toString(); }
  int32_t toInteger() const { return fData->toInteger(); }
  double toDouble() const { return fData->toDouble(); }

 private:
  ExpensiveInternalObject* fData;
}

If you want the string, you can use it like this:

// Example A
std::string resultString = Result::compute(/*...*/).toString();

If you want more than one of the return types, you do it like this:

// Example B
Result result = Result::compute(/*...*/);
std::string resultString = result.toString();
int32_t resultInteger = result.toInteger();

Everything works.

However, I want to modify this class such that there is no need to allocate memory on the heap if the user needs only one of the result types. For example, I want Example A to essentially do the equivalent of,

auto result = ExpensiveInternalObject(/* args */);
std::string resultString = result.toString();

I've thought about structuring the code such that the args are saved into the instance of Result, make the ExpensiveInternalObject not be calculated until the terminal functions (toString/toInteger/toDouble), and overload the terminal functions with rvalue reference qualifiers, like this:

class Result {
  // ...
  std::string toString() const & {
    if (fData == nullptr) {
      const_cast<Result*>(this)->fData = new ExpensiveInternalObject(/*...*/);
    }
    return fData->toString();
  }
  std::string toString() && {
    auto result = ExpensiveInternalObject(/*...*/);
    return result.toString();
  }
  // ...
}

Although this avoids the heap allocation for the Example A call site, the problem with this approach is that you have to start thinking about thread safety issues. You'd probably want to make fData an std::atomic, which adds overhead to the Example B call site.

Another option would be to make two versions of compute() under different names, one for the Example A use case and one for the Example B use case, but this isn't very friendly to the user of the API, because now they have to study which version of the method to use, and they will get poor performance if they choose the wrong one.

I can't make ExpensiveInternalObject a value field inside Result (as opposed to a pointer) because doing so would require exposing too many internals in the public header file.

Is there a way to make the first function, compute(), know whether its return value is going to become an rvalue reference or whether it is going to become an lvalue, and have different behavior for each case?


Solution

  • You can achieve the syntax you asked for using a kind of proxy object.

    Instead of a Result, Result::compute could return an object that represents a promise of a Result. This Promise object could have a conversion operator that implicitly converts to a Result so that "Example B" still works as before. But the promise could also have its own toString(), toInteger(), ... member functions for "Example A":

    class Result {
     public:
       class Promise {
         private:
          // args
    
         public:
         std::string toString() const { 
            auto result = ExpensiveInternalObject(/* args */);
            return result.toString(); 
         } 
         operator Result() { 
            Result result;
            result.fData = new ExpensiveInternalObject(/* args */);
            return result;
         }
       };
    
       // ...
    
    };
    

    Live demo.

    This approach has its downsides though. For example, what if, instead you wrote:

     auto result = Result::compute(/*...*/);
     std::string resultString = result.toString();
     int32_t resultInteger = result.toInteger();
    

    result is now not of Result type but actually a Result::Promise and you end up computing ExpensiveInternalObject twice! You can at least make this to fail to compile by adding an rvalue reference qualifier to the toString(), toInteger(), ... member functions on Result::Promise but it is not ideal.