Rules around RVO

Hi

Just to confirm:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct Block
{
	struct DataWriter
	{
		DataWriter(Block* owner) : mOwner(owner) {}
		DataWriter(DataWriter&& other) : mOwner(other.mOwner) { other.mOwner = nullptr; }

		~DataWriter()
		{
			if (!mOwner)
				return;
                       //do something with owner
		}

		Block* mOwner;
	};

	DataWriter GetWriteHandle() { return DataWriter(this); }
};


In the code segment above, is the move semantics stuff in DataWriter necessary? I want a guarantee that RAII on the owner Block is done just once in such a block of code:

1
2
3
4
5
6
7
8
9
int main()
{
	Block block;
	{
		auto handle = block.GetWriteHandle();
	}

    return 0;
} 


Can someone point me to an explanation of how a compiler is allowed to optimize this, and what assumptions a programmer can make?
Last edited on
> Can someone point me to an explanation of how a compiler is allowed to optimize this,
> and what assumptions a programmer can make?

Copy-elision (RVO) is mandatory in C++17, and permitted but not required in C++14.

In C++14, every mainstream compiler implements RVO when optimisations are enabled,
but every compiler may not apply the optimisation when optimisations are disabled.

For details, see: http://en.cppreference.com/w/cpp/language/copy_elision
Thanks. I notice it says:

"which isn't the function parameter, "

What about when the function parameter is a copy?
error C2280: 'Block::DataWriter::DataWriter(Block::DataWriter &&)': attempting to reference a deleted function

Cannot mark the move/copy constructors as deleted. Seems it is needed to do this for completeness (in msvc14.1 at least). Not called even in debug mode though.

Maybe could wrap such stuff in:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

template<typename T>
class DestroyOnce
{
	char mMemory[sizeof(T)];
	bool mMoved = false;
public:
	template<typename... Args>
	DestroyOnce(Args&&... args) 
	{
		new (mMemory) T(std::forward<Args>(args)...);
	}

	DestroyOnce(DestroyOnce&& other)
	{
		std::copy(std::begin(other.mMemory), std::end(other.mMemory), std::begin(mMemory));
		other.mMoved = true;
	}

	~DestroyOnce()
	{
		if (!mMoved)
			static_cast<T&>(*this).~T();
	}

	operator T&() { return reinterpret_cast<T&>(mMemory); }
	operator const T&() const { return reinterpret_cast<const T&>(mMemory); }
};


So you get

 
DestroyOnce<DataWriter> GetWriteHandle() { return DestroyOnce<DataWriter>(*this); }


Still uses RVO on good compiler, but cuts down on having to put in boiler plate for every RAII class of this type.
Last edited on
> What about when the function parameter is a copy?

In a return statement, if copy-elision is not possible only because the source of the copy is a function parameter, the compiler will use the move constructor (if one is available), even on an lvalue.



> Cannot mark the move/copy constructors as deleted.

For DataWriter GetWriteHandle() { return DataWriter(this); }

It is only C++17 that does not require that there must be an accessible copy/move constructor.
(This optimisation is mandated by the standard; there is no copy/move because every implementation is required to elide the copy.)

In earlier versions of C++, even if the optimisation does take place, and no copy/move constructor is actually called, there must still be an accessible copy/move constructor. (ie. the program must be well-formed, irrespective of whether an implementation performs this optimisation or not.)
Got it thanks.

"even on an lvalue"

Makes sense, that's a good one to know lol
Topic archived. No new replies allowed.