【问题标题】:Construct returned object in calling function's scope在调用函数的范围内构造返回的对象
【发布时间】:2015-01-29 11:20:48
【问题描述】:

是否可以强制 C++ 在调用函数的范围内构造对象?我的意思是明确地做返回值优化 (RVO) 所做的事情。

我有一些容器类在派生链中。由于类是用堆栈数据构造的,因此无法返回,因此我禁用了复制构造函数和赋值运算符。对于每个类,我都提供了一个迭代器。每个迭代器的构造函数只有一个参数:指向容器类的指针。要获取迭代器,我想使用这个函数:

BindPackIterator BindPack.begin(void)
{
    return BindPackIterator(this);
}

在这种情况下:

for (auto i=bindpack.begin(); !i.end(); ++i) { i.run(); }

编译器发出错误,抱怨无法复制 BindPackIterator 对象。请记住,我禁用了它们。

我想要在调用函数的范围内实例化 BindPackIterator 以避免复制或移动操作。

在这种特殊情况下,我知道我可以做一个解决方法,将 begin 函数更改为返回一个 BindPack 指针,

for(BindPackIterator i=bindpack.begin(); !i.end(); ++i) { i.run(); }

我用 decltype 和这个结构做了一些实验,但没有成功:

auto BindPack::begin(void) -> BindPackIterator
{
    return BindPackIterator(this);
}

这只是我目前感到沮丧的例子。在其他项目中,明显的解决方案是让函数在调用函数的范围内实例化一个对象。移动构造函数 (foo&&) 在某些情况下会有所帮助,但对于具有许多数据成员的对象,即使这样也可能效率低下。是否有允许在调用者范围内构造/实例化对象的设计模式?

【问题讨论】:

  • 我可以理解为什么您可能希望在容器类上禁用复制但迭代器类应该非常小,为什么不能复制/移动?
  • 迭代器应该是可复制的并且最好是可移动的。如果您的两者都不是,那么某处的设计可能有问题。关于调用者范围内的构造,这将是一个in/out 参数,对于迭代器来说相当奇怪。无论您是否正确编码,RVO 都应该处理它。
  • 不可复制的迭代器没有任何意义。只是不要那样做。
  • 如果您希望您的客户构造类 X 的对象,请提供类 X 的构造函数,而不是返回 X 的函数。
  • 我会尝试换一种方式。您希望BindPack.begin 直接构造一个对象,而不是构造一个对象并返回一个副本。编写一个与BindPack.begin 做同样事情的构造函数。

标签: c++ optimization rvo


【解决方案1】:

将 n.m. 的注释放入代码中,为 BindPackIterator 编写一个构造函数,该构造函数接受 BindPack 并将迭代器初始化为“开始”状态。例如:

BindPackIterator(BindPack* pack) : pack(pack), pos(0){ }

你可以在你的 for 循环中使用:

BindPack pack;

for(BindPackIterator i(&pack); !i.end(); ++i){
  i.run();
}

Live demo

【讨论】:

  • 嘿,我刚刚注意到并尝试了“现场演示”。我将来会使用它。谢谢!
【解决方案2】:

如果说答案是“否”,那么在调用函数的范围内不可能构造返回的对象,这是否公平?或者换句话说,你不能明确告诉编译器使用 RVO。

可以肯定的是,这是一种危险的可能性:用于构造对象的堆栈内存在被调用函数中可用时将在调用函数中无效,即使这些值可能在废弃的堆栈帧中保持不变。这会导致不可预知的行为。

经过进一步考虑,在总结此响应的末尾时,我意识到编译器可能无法准确预测在调用函数中创建并在被调用函数中初始化的对象的必要堆栈大小,并且它会如果执行已传递给另一个函数,则无法动态扩展堆栈帧。这些考虑使我的整个想法变得不可能。

也就是说,我想解决解决我的迭代器示例的变通办法。

我不得不放弃像这样使用auto 的想法:

for (auto i=bindpack.begin(); !i.end(); ++i)

已经放弃auto,并意识到明确命名变量更明智(如果迭代器不同到需要一个新类,最好命名它以避免混淆),我正在使用这个构造函数:

BindPackIterator(BindPack &ref) : m_ref_pack(ref), m_index(0) { }

为了能写:

for (BindPackIterator i=bindpack; !i.end(); ++i)

更喜欢用赋值来初始化。当我在 1990 年代后期最后一次大量使用 C++ 时,我曾经这样做过,但它最近对我不起作用。由于上述原因,编译器会要求我不想定义一个复制运算符。现在我认为这个问题是由于我为通过 -Weffc++ 测试而定义的构造函数和赋值运算符的集合。在本示例中使用简化的类使其能够工作。

对于比迭代器更复杂的对象,另一种解决方法可能是为需要多个变量来初始化的对象的构造函数参数使用元组。可能有一个强制转换运算符从初始化对象的类中返回必要的元组。

构造函数可能如下所示:

FancyObject(BigHairyTuple val) : m_data1(get<0>(val)), m_data2(get<1>(val), etc

贡献对象会定义这个:

class Foo
{
    ...
    operator BigHairyTuple(void) {
        return BigHairyTuple(val1, val2, ...);
    }
};

允许:

FancyObject fo = foo;

我还没有测试过这个具体的例子,但我正在使用类似的东西,它似乎可以工作,并进行一些可能的小改进。

【讨论】:

  • 当我发布我的答案时,我还没有弄清楚到底发生了什么。我比我希望的更早做出回应现在我看到FancyObject fo = foo 没有按我的预期工作,事实上,当我像在我的示例中那样删除复制运算符时失败了。我希望它直接使用转换构造函数,但似乎编译器在赋值运算符的右侧应用了转换构造函数,然后在第二步中使用了覆盖的赋值运算符。
猜你喜欢
  • 2016-01-23
  • 1970-01-01
  • 2011-12-29
  • 2013-05-05
  • 1970-01-01
  • 2021-06-17
  • 1970-01-01
  • 2013-10-03
  • 1970-01-01
相关资源
最近更新 更多