【问题标题】: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?如果函数的返回值将用作右值引用而不是左值,是否有办法使函数具有不同的行为?
【发布时间】:2018-09-17 03:13:12
【问题描述】:

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

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();

如果你想要一种以上的返回类型,你可以这样做:

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

一切正常。

然而,我想修改这个类,这样如果用户只需要一种结果类型,就不需要在堆上分配内存。例如,我希望示例 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(),一个用于示例 A 用例,一个用于示例 B 用例,但这对 API 的用户不太友好,因为现在他们必须研究使用哪个版本的方法,如果选择错误的方法会导致性能不佳。

我不能在 Result 中创建一个值字段 ExpensiveInternalObject(而不是指针),因为这样做需要在公共头文件中暴露太多内部结构。

有没有办法让第一个函数compute() 知道它的返回值是要变成右值引用还是要变成左值,并且在每种情况下都有不同的行为?

【问题讨论】:

  • 我假设您已经测量并且在堆而不是堆栈上分配ExpensiveInternalObject 的开销很大吗?根据我的经验,任何“中等开销的操作”通常都会包括像 std::vector 这样涉及堆分配的操作,因此额外的堆分配通常是微不足道的。
  • 你可能想看看The Fast Pimpl Idiom
  • 您是否考虑过返回具有多种类型的std::tuple?类似auto result = Result::compute&lt;std::string, int32_t, double&gt;(args...);

标签: c++ api-design rvalue-reference


【解决方案1】:

考虑到you can't overload 的返回类型是一个函数,并且您想避免创建compute() 的两个不同版本,我唯一能想到的就是在Result 的复制构造函数中设置一个标志。这可能适用于您的特定示例,但不适用于一般情况。例如,如果您要引用 you can't disallow,它将不起作用。

【讨论】:

    【解决方案2】:

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

    代替ResultResult::compute 可以返回一个代表Result 承诺的对象。这个Promise 对象可以有一个隐式转换为Result 的转换运算符,因此“示例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,但这并不理想。

    【讨论】:

    • 聪明的主意!您说向 Result::Promise 函数添加右值引用限定符是“不理想的”;你能详细说明吗?将左值限定符 (&) 放在 Result 函数上,然后在 Result::Promise 函数上放置右值限定符 (&&) 似乎很干净。
    • 我猜“不理想”的部分是编译器错误消息相当神秘。嗯。
    • @sffc 这与std::vector&lt;bool&gt;::operator[] 使用的方法类似,通常被认为是错误的。
    • 另一种选择:toString 方法预计是最常见的,因此可以在compute 旁边添加静态computeToString 方法。它不会像其他一些选项那样承担不当使用的风险,但代价是添加另一个功能并让用户阅读文档以了解哪个更好。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-06-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-01-27
    • 1970-01-01
    • 2019-03-29
    相关资源
    最近更新 更多