0

When running the following code:

struct Copy
{
    Copy() {std::cerr << __PRETTY_FUNCTION__ << std::endl;}
    Copy(const Copy & other) {std::cerr << __PRETTY_FUNCTION__ << std::endl;}
    Copy & operator=(const Copy & other) {std::cerr << __PRETTY_FUNCTION__ << std::endl; return *this;}
    Copy(Copy && other) {std::cerr << __PRETTY_FUNCTION__ << std::endl;}
    Copy & operator=(Copy && other) {std::cerr << __PRETTY_FUNCTION__ << std::endl; return *this;}
    ~Copy() {std::cerr << __PRETTY_FUNCTION__ << std::endl;}
};

char buffer[1024];

template <typename Type>
Type * push(Type value)
{
    return new(buffer) Type(std::move(value));
};

int main()
{
    push(Copy());

    return 0;
}

I get the following output:

Copy::Copy()
Copy::Copy(Copy &&)
Copy::~Copy()

Is there anyway to elide the move constructor?

I was hoping that with -O3, it would be constructed in place, but by my testing, that doesn't seem to be the case.

ioquatix
  • 1,411
  • 17
  • 32

3 Answers3

3

Is there anyway to elide the move constructor? [...] I was hoping that with -O3, it would be constructed in place,

Well, you're explicitly calling the move constructor... and the object is being constructed in-place in buffer. Why do you expect the move constructor call to be elided?

template <typename Type>
Type * push(Type value)
{ 
    //                 invokes `Type::Type(Type&&)`
    //                 vvvvvvvvvvvvvvvvvvvvvv
    return new(buffer) Type(std::move(value));
    //                      ^^^^^^^^^^^^^^^^
    //                      casts `value` to `Type&&`
};

Your question would make more sense if you were trying to construct Copy with a value of some other type. E.g.:

struct Copy
{
    Copy(int) { std::cout << "INT\n"; }
    // ... other ctors ...
};

template <typename Type, typename Arg>
Type * push(Arg x)
{
    return new(buffer) Type(std::move(x));
};

int main()
{
    push<Copy>(1);
}

The code above prints:

INT

No move/copy constructor is invoked.

live example on wandbox

Vittorio Romeo
  • 90,666
  • 33
  • 258
  • 416
  • Thanks, yes, you are absolutely right, `std::move` is causing some issues. The problem is, without it, it invokes the copy operator, and it doesn't get elided at any -O level. – ioquatix Jul 04 '17 at 13:08
  • 1
    @ioquatix: `std::move` is not really causing any issue - I don't understand what you're trying to do - you're explicitly calling the constructor of `Copy`... but expecting it not to be called? – Vittorio Romeo Jul 04 '17 at 13:10
  • I was hoping the compiler would elide it. – ioquatix Jul 04 '17 at 13:10
  • 1
    @ioquatix: your're basically asking the compiler to elide the call to `foo()` here `void foo(); int main(){ foo(); }`. Just think of `void foo()` as `Copy::Copy(Copy&&)`. You are explicitly calling that constructor, which is executing a side-effect. How could it ever be elided... and why? – Vittorio Romeo Jul 04 '17 at 13:16
  • Frankly speaking, the rule surrounding elision and how to handle it are not easy to understand. I appreciate you are giving me some guidelines to follow/think about. I managed to get down to a single move constructor in my specific case, by using what you suggested. You are very generous with your time and patience. – ioquatix Jul 04 '17 at 13:21
  • Just to clarify - I have a situation where there requires at least one move/copy. But, the code in this question would cause two. However, using emplace, can reduce back to 1 (i.e. the overhead goes to 0, as in push requires one move/copy but emplace zero). – ioquatix Jul 04 '17 at 13:23
  • Also, I appreciate, sometimes it's hard to ask the right question here, to give a minimal example - that it must be confusing if you don't see the big picture, but to include all the code would make the problem hard to discuss. So I tried to keep it minimal. However, despite this, your answer and thoughts were helpful. – ioquatix Jul 04 '17 at 13:24
2

I do not think you can do this. Because elision requires the compiler to have an intrinsic knowledge of where the objects are being constructed. And with that information, it can just avoid moves and copies and simply place the object where it needs to go. For example when you return something from the stack of one function back to another, the compiler can elide the move/copy.

But in your case placement new allows you to construct an object into any arbitrary buffer. And that buffer can really be anywhere, for example it can be on the stack (like in your case) or it can be allocated on the heap with new. So the compiler does not elide the move/copy here.

Strictly speaking, this can happen through some analysis of the code since the compiler already knows where the buffer is, but I doubt most compilers implement this.


Note note that you have declared an array of character pointers and not characters, so the buffer is more than 1024 bytes in length if that is what you were expecting


Note Also note that calling std::move explicitly can prevent elision


The best you can do in this case is make an in place constructor or a move constructor (as you have above) to construct that object into the buffer. An in place constructor would look something like this

template <typename... args>
void construct(std::in_place_t, Args&&... args) {
    new (buffer) Type{std::forward<Args>(args)...};
}
Curious
  • 20,870
  • 8
  • 61
  • 146
  • Haha - I laughed when I looked at the char * buffer[1024]. That's hilarious :D Thanks for all the other information. Yes, I've also tried without `std::move` but it didn't make any difference. – ioquatix Jul 04 '17 at 12:53
1

Use an emplace function with perfect argument forwarding. There are a few more things to say since you're about to embark on an adventure in placement new:

  1. Use std::aligned_storage_t<> for the storage. It guarantees that your objects will be properly aligned.

  2. Do read and use the return value of placement new. In simple cases it won't be different from the address you provide. However the standard allows it to be, and in complex class hierarchies it might be.

updated example:

#include <iostream>

struct Copy
{
    Copy() {std::cerr << __PRETTY_FUNCTION__ << std::endl;}
    Copy(const Copy & other) {std::cerr << __PRETTY_FUNCTION__ << std::endl;}
    Copy & operator=(const Copy & other) {std::cerr << __PRETTY_FUNCTION__ << std::endl; return *this;}
    Copy(Copy && other) {std::cerr << __PRETTY_FUNCTION__ << std::endl;}
    Copy & operator=(Copy && other) {std::cerr << __PRETTY_FUNCTION__ << std::endl; return *this;}
    ~Copy() {std::cerr << __PRETTY_FUNCTION__ << std::endl;}
};

std::aligned_storage_t<sizeof(Copy), alignof(Copy)> buffer[4];

template <typename Type, typename LegalStorage, typename...Args>
auto emplace(LegalStorage* buffer, Args&&...args) -> Type*
{

    return new(buffer) Type(std::forward<Args>(args)...);
};

int main()
{
    auto p1 = emplace<Copy>(buffer /* constructor arguments go here*/);
    auto p2 = emplace<Copy>(&buffer[1]/* constructor arguments go here*/);
    auto p3 = emplace<Copy>(buffer + 2/* constructor arguments go here*/);
    auto p4 = emplace<Copy>(buffer + 3/* constructor arguments go here*/);

    return 0;
}
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142