Search code examples
c++templatesconstructor

Why can I construct from a different struct with the copy/move constructors deleted?


I am defining two structs and deleting the copy/move constructors from both, but I somehow still can construct something that I should not be able to. Long story short, I am trying to understand how can I construct const_Obj<Float, 1> from const_Obj<float, 1> when the copy/move constructors have been deleted. The only constructor is the one with Obj not const_Obj. Similarly, I have narrowed down the constructors for Obj to the one with const_Obj<T, size> const& that still allows this to happen.

I suspect it somehow thinks I have a conversion operator for both and does it in two steps something like:

const_Obj( Obj<float, 1>&&(const_Obj<Float, 1> const&) )

but I wasn't aware that the compiler can convert in multiple steps like this. I'm guessing this is working only because it's an rvalue reference meaning it just "renames" the variable and can "skip" this conversion to Obj straight to const_Obj?

Edit: I can achieve the desired behavior by adding explicit to the Obj constructor. Still a surprising possibility from implicit construction (conversion operator)?

code on godbolt

#include <iostream>
#include <concepts>

template <typename T, int size>
class Obj;

template <typename T, int size>
class const_Obj;

template <typename value_type, int size>
class Obj
{
public:
    Obj(Obj const& other) {}
    Obj(Obj&& other) {}
    auto operator=(Obj const& other) {}
    auto operator=(Obj&& other) {}

    template <typename T> requires std::assignable_from<value_type&, T> // <------------------------?!
    Obj(Obj<T, size> const& other) = delete;

    template <typename T> requires std::assignable_from<value_type&, T> // <------------------------?!
    Obj(Obj<T, size>&& other) = delete;

    template <typename T> requires std::assignable_from<value_type&, T> // <------------------------?!
    auto operator=(Obj<T, size> const& other) = delete;

    template <typename T> requires std::assignable_from<value_type&, T> // <------------------------?!
    auto operator=(Obj<T, size>&& other) = delete;

    template <typename T> requires std::assignable_from<value_type&, T> // this acts somehow like a convertion operator
    Obj(const_Obj<T, size> const& other) {}

    template <typename T> requires std::assignable_from<value_type&, T> // <------------------------?!
    Obj(const_Obj<T, size>&& other) = delete;

    template <typename T> requires std::assignable_from<value_type&, T> // <------------------------?!
    auto operator=(const_Obj<T, size> const& other) = delete;

    template <typename T> requires std::assignable_from<value_type&, T> // <------------------------?!
    auto operator=(const_Obj<T, size>&& other) = delete;

private:
    value_type data;
};

template <typename value_type, int size>
class const_Obj
{
public:
    const_Obj(const_Obj const& other) = delete;                     // <------------------------?!
    const_Obj(const_Obj&& other) = delete;                          // <------------------------?!
    auto operator=(const_Obj const& other) = delete;                // <------------------------?!
    auto operator=(const_Obj&& other) = delete;                     // <------------------------?!

    const_Obj(Obj<value_type, size> const& other) = delete;         // <------------------------?!
    const_Obj(Obj<value_type, size>&& other) {}                     // if = delete; it is no longer constructible from different value
    auto operator=(Obj<value_type, size> const& other) = delete;    // <------------------------?!
    auto operator=(Obj<value_type, size>&& other) = delete;         // <------------------------?!

private:
    value_type data;
};

struct Float
{ 
    Float(float data) : data{ data } {}
    auto& operator=(float data) { this->data = data; return *this; }
    float data; 
};

int main()
{
    static_assert(std::assignable_from<Float&, float>);

    // this assert should pass
    // I shouldn't be able to construct from the same object with different template parameter value_type
    // because it an operator that wasn't defined... Why can it do that?
    static_assert(not std::constructible_from<const_Obj<Float, 1>, const_Obj<float, 1> const&>);
}

Solution

  • First of all, you are constructing const_Obj<Float, 1> from const_Obj<float, 1> which makes copy/move operations irrelevant. These are TWO DIFFERENT types and copy/move constructor is NOT involved.

    Compiler just needs a constructor and (possibly) a single user defined conversion to make it work.

    What I think is happening is:

    1. Compiler constructs temporary Obj<Float, 1> from const_Obj<float, 1> using template <typename T> Obj(const_Obj<T, size> const& other) constructor, doing instantiation with T = float as Float is assignable from float.
    2. Then it calls const_Obj(Obj<value_type, size>&& other) on temporary Obj it created in previous step.