【发布时间】: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<std::string, int32_t, double>(args...);
标签: c++ api-design rvalue-reference