Move only type adapting std::any with dummy copy constructor is safe?
Asked Answered
D

1

9

I'd like to initialize std::any with a move only type variable. I found Cannot move std::any.

Compile error case

Before I use shared_ptr workaround from the linked answer, I tested the following code:

#include <utility>
#include <iostream>
#include <any>

struct move_only {
    move_only() {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
    }
    move_only(move_only const&) = delete;
    move_only(move_only &&) {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
    }
};

int main() {
    move_only m;
    std::any a(std::move(m)); // error. copy constructor is required
}

https://wandbox.org/permlink/h6HOSdgOnQYg4a6K

The code above outputs compile error because of move_only doesn't have copy constructor.

Add copy constructor for test

I added copy constructor for test.

#include <utility>
#include <iostream>
#include <any>

struct move_only {
    move_only() {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
    }
    move_only(move_only const&) {
        // not called
        std::cout << __PRETTY_FUNCTION__ << std::endl;
    }
    move_only(move_only &&) {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
    }
};

int main() {
    move_only m;
    std::any a(std::move(m)); // success but copy constructor is not called
}

https://wandbox.org/permlink/kxEnIslmVnJNRSn6

Then compile successfully finished as I expected. And I got interesting output.

move_only::move_only()
move_only::move_only(move_only &&)

It seems that the copy constructor isn't called. It is surprising for me.

And I come up with the following wrapper approach.

Add dummy copy constructor wrapper

#include <utility>
#include <iostream>
#include <any>

struct move_only {
    move_only() {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
    }
    move_only(move_only const&) = delete;
    move_only(move_only &&) {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
    }
};

struct wrapped_move_only : move_only {
    wrapped_move_only(move_only&& m):move_only(std::move(m)) {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
    }
    wrapped_move_only(wrapped_move_only const&) {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
        assert(false);
    }
    wrapped_move_only(wrapped_move_only &&) = default;

};

int main() {
    move_only m;
    wrapped_move_only wmo(std::move(m));
    std::any a(std::move(wmo));
}

https://wandbox.org/permlink/EDhq3KPWKP9fCA9v

The copy constructor of move_only is deleted. The class wapped_move_only inherits move_only and added copy constructor.

It successfully compiled and I got the following result.

move_only::move_only()
move_only::move_only(move_only &&)
wrapped_move_only::wrapped_move_only(move_only &&)
move_only::move_only(move_only &&)

It seems that I initialized std::any with move only type using the wrapper that provides dummy copy constructor. It is more efficient to use shared_ptr if the goal is just initialize std::any with move only type. It is expected behavior for me.

As long as I do move operation only for std::any once move_only is moved to std::any, is this code safe? If std::any is copied, then assetion failed due to copy constructor of wrapped_move_only is called. I want to know move only case's safety.

I am also not sure why std::any's target requires copy constructor but it is not called.

templatized

If it is safe, I can improve this approach using template. The template add_dummy_copy_constructor is a kind of adaptor.

#include <utility>
#include <iostream>
#include <any>

struct move_only {
    move_only() {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
    }
    move_only(move_only const&) = delete;
    move_only(move_only &&) {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
    }
};

template <typename T>
struct add_dummy_copy_constructor : T {
    add_dummy_copy_constructor(T&& t):T(std::move(t)) {}
    add_dummy_copy_constructor(add_dummy_copy_constructor const&) {
        assert(false);
    }
    add_dummy_copy_constructor(add_dummy_copy_constructor &&) = default;
};

int main() {
    move_only m;
    std::any a(add_dummy_copy_constructor(std::move(m)));
}

https://wandbox.org/permlink/guEWPIrK9wiJ3BgW

Dougald answered 13/9, 2019 at 12:21 Comment(0)
S
4

I am also not sure why std::any's target requires copy constructor but it is not called.

The design of std::any is to be one concrete type that can hold any copyable type. When you copy a std::any, you copy whatever it is it's holding underneath

The std::any needs to know how to copy the underlying object regardless of if it is ever actually going to be copied (how would it know whether or not this happens?). So it must be a compile error if the underlying type isn't copy-constructible.

However, when we're constructing the std::any itself, at that point we know the concrete object we're constructing from. And if that concrete object happens to be an rvalue, then we can just move-construct std::any's underlying object from the constructor parameter rather than copy-constructing. It's a free win there.

None of your code actually copies a std::any, so none of it would invoke std::any's copy constructor which would invoke the underlying types copy constructor.


A similar thing happens with std::function and maybe it would be more obvious what the difference here is. When I construct a std::function<void()>, there is the static requirement that the object be invokable with no arguments. It is a compile error if it is not invokabe.

But simply constructing the std::function<void()> will not actually invoke the underlying function - those are separate operations. You wouldn't expect this assertion to trigger:

std::function<void()> f = []{ assert(false); }
Sinuate answered 13/9, 2019 at 13:4 Comment(1)
thank you for the answer. I understand why. When I read your answer at first, I thought that we could std::any's copy constructor conditionally enable/disable depends on actual type but I realized that std::any is type erased type. The actual type is decided at run time. std::any provides copy constructor is reasonable choice. So the actual type need to have copy constructor.Dougald

© 2022 - 2024 — McMap. All rights reserved.