如果函数的 return 值将用作右值引用而不是左值,是否有办法使函数具有不同的行为?

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?

我有一个执行一些中等开销操作的例程,客户端可以将结果作为字符串、整数或许多其他数据类型使用。我有一个 public 数据类型,它是内部数据类型的包装器。我的 public class 看起来像这样:

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;
}

如果你想要字符串,你可以这样使用:

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

如果您想要不止一种 return 类型,您可以这样做:

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

一切正常。

但是,我想修改这个class,如果用户只需要一种结果类型,就不需要在堆上分配内存。例如,我希望示例 A 本质上相当于

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

我考虑过构建代码,以便将 args 保存到 Result 的实例中,使 ExpensiveInternalObject 在终端函数 (toString/ toInteger/toDouble),并使用右值引用限定符重载终端函数,如下所示:

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();
  }
  // ...
}

虽然这避免了示例 A 调用站点的堆分配,但这种方法的问题是您必须开始考虑线程安全问题。您可能希望将 fData 设为 std::atomic,这会增加示例 B 调用站点的开销。

另一种选择是制作两个不同名称的compute()版本,一个用于Example A用例,一个用于Example B用例,但这对用户不是很友好API,因为现在他们必须研究使用哪个版本的方法,如果选择错误,他们的表现会很差。

我不能在 Result 中创建 ExpensiveInternalObject 值字段(与指针相反),因为这样做需要在 public 头文件中暴露太多内部结构。

有没有办法让第一个函数compute()知道它的return值是要变成右值引用还是要变成左值,并且有不同的每个案例的行为?

正在考虑 you can't overload a function by its return type, and you wanted to avoid making two different versions of compute(), the only thing I can think of is setting a flag in the copy constructor of Result. This could work with your particular example, but not in general. For example, it won't work if you're taking a reference, which you can't disallow

您可以使用一种代理对象实现您要求的语法。

Result::compute 可以 return 一个表示 Result 承诺的对象,而不是 Result。这个 Promise 对象可以有一个转换运算符,隐式转换为 Result,这样 "Example B" 仍然像以前一样工作。但是 promise 也可以有自己的 toString(), toInteger(), ... "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.

虽然这种方法有其缺点。例如,如果您改为这样写:

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

result 现在不是 Result 类型,而是 Result::Promise 类型,您最终会计算 ExpensiveInternalObject 两次!您至少可以通过向 toString()toInteger()、... Result::Promise 上的成员函数添加右值引用限定符来实现 fail to compile,但这并不理想。