1

Context: In an attempt to implement the copy and swap idiom on a templated homemade shared_ptr. I'm having trouble to get this function called:

    template<typename U>
    shared_ptr<T>& operator=(shared_ptr<U> rhs)
    {
        static_assert(std::is_base_of<T, U>::value);
        swap(*this, rhs);
        return *this;
    }

Code stripped from the unecessary:

The following code does not compile. The "x=y" line is an attempt to use:

    foo<T>& operator=(const foo<U>& ptr) = delete;

If I remove/comment the deleted functions line. Meaning if I do not define them, the compiler will do it for me and the code will compile. However,the special member generated by the compiler will be preferred to my operator which will not be called.

template<typename T> 
class foo
{
public:

    // copy&swap
    template<typename U>
    foo<T>& operator=(foo<U> rhs)
    {
        // whatever is needed swap and all
        return *this;
    }

    template<typename U>
    foo<T>& operator=(const foo<U>& ptr) = delete;
    template<typename U>
    foo<T>& operator=(foo<U>&& ptr) = delete;

    foo<T>& operator=(foo<T>&& ptr) = delete;
};

int main()
{
    foo<int> x,y;
    x = y;
    return 0;
}

Questions:

  • The overload resolution is not clear to me here. Why is one signature preferred to the other?
  • If the function is deleted, I understand it is not a "substitution failure", but still it is a failure at signature level, not inside the body, why does the compiler stop looking for a match?
  • Any solution to get the function to be called ?


Edit/Answer

This is the correct version so far:

  • Non-template constructors (move/copy) are marked explicit so that assigning foo into foo calls the template version.
  • Appropriate static assert are added so conversion U/T makes sense
  • Arguments are perfect forwarded to the assign/move internal functions

code:

#include <iostream>
template<typename T>
class foo
{
private:
    template <typename U> friend class foo;

    template<typename U>
    void assign(const foo<U>& other)
    {
        static_assert(std::is_base_of<T, U>::value || std::is_convertible<U, T>::value);
        std::cout << "templated assign function" << std::endl;
        this->data = other.data;
    }

    template<typename U>
    void move(foo<U>&& other)
    {
        static_assert(std::is_base_of<T, U>::value || std::is_convertible<U, T>::value);
        std::cout << "templated move function" << std::endl;
        this->swap(*this, other);
    }
public:

    template<class X, class Y> void swap(foo<X>& left, foo<Y>& right) noexcept
    {
        std::cout << "templated swap function" << std::endl;
        std::swap(left.data, right.data);
    }

    void swap(foo<T>& left, foo<T>& right) noexcept
    {
        std::cout << "swap function" << std::endl;
        std::swap(left.data, right.data);
    }

    foo() {}

    explicit foo(foo<T>&& other)
    {
        std::cout << "move constructor foo(foo&& other)" << std::endl;
        move(std::forward<decltype(other)>(other));
    }


    explicit foo(const foo<T>& other)
    {
        std::cout << "copy constructor foo(const foo& other)" << std::endl;
        assign(std::forward<decltype(other)>(other));
    }

    template<typename U>
    foo(foo<U>&& other)
    {
        static_assert(std::is_base_of<T, U>::value || std::is_convertible<U, T>::value);
        std::cout << "templated move constructor template<typename U> foo(foo<U>&& other)" << std::endl;
        move(std::forward<decltype(other)>(other));
    }

    template<typename U>
    foo(const foo<U>& other)
    {
        static_assert(std::is_base_of<T, U>::value || std::is_convertible<U,T>::value);
        std::cout << "templated copy constructor template<typename U> foo(const foo<U>& other)" << std::endl;
        assign(std::forward<decltype(other)>(other));
    }

    // copy&swap
    template<typename U>
    foo<T>& operator=(foo<U> rhs)
    {
        static_assert(std::is_base_of<T, U>::value || std::is_convertible<U, T>::value);

        std::cout << "templated assignement template<typename U> foo<T>& operator=(foo<U> rhs)" << std::endl;

        auto tmp = rhs.data;
        rhs.data = reinterpret_cast<U*>(data);
        data = reinterpret_cast<T*>(tmp);

        return *this;
    }

    foo<T>& operator=(foo<T> rhs)
    {
        std::cout << "assignement foo<T>& operator=(foo<T> rhs)" << std::endl;

        std::swap(rhs.data,data);
        return *this;
    }

private:
    T * data;
};

int main()
{
    foo<int> x,y;
    const foo<int>& cy = y;

    foo<short> z, w;
    x = y;
    x = cy;
    x = std::move(y);
    x = z;
    x = std::move(w);
    return 0;
}
user2346536
  • 1,316
  • 2
  • 16
  • 40
  • 2
    Not totally clear about what you are asking, but some hints anyway: First `= delete` doesn't mean "ignore this", but "this operation is not allowed". The compiler doesn't go looking for an alternate match. And if you have a template and a non-template that both match equally well, the non-template will win the tie-break. That gives the compiler generated functions an advantage. – Bo Persson May 27 '18 at 11:02
  • @BoPersson thanks, that explains the overload problem. My main question is "how to get my operator to be called"... because an automatically generated function will be preferred to it, and I do not see how to prevent this... – user2346536 May 27 '18 at 11:11
  • 1
    I don't think that your solution is a correct interpretation of the copy/swap idiom. And even if you check ´is_convertible || is_base_of´ the ´reinterpret_cast´ is still dangerous and may fail in case of multiple inheritance. – kiloalphaindia May 30 '18 at 13:10
  • You might be right because of the reinterpret casts... I'll look for a solution to this – user2346536 May 31 '18 at 13:54

1 Answers1

1
  1. the operator=(const foo&) only differs in cv qualifier and will be preferred from the template
  2. = delete causes the operator=(const foo&) to be forbidden. It is not only stopping the compiler from generating it.

This should do it:

#include <tuple>
#include <iostream>

template<typename T>
class foo
{
private:
    template <typename U> friend class foo;

    foo& assign(T rhs)
    {
        std::swap(data, rhs);
        return *this;
    }

public:


    template<typename U>
    foo& operator=(const foo<U>& rhs)
    {
        return assign(rhs.data);
    }

    template<typename U>
    foo& operator=(foo<U>&& rhs)
    {
        return assign(std::move(rhs.data));
    }

    foo& operator=(const foo& rhs)
    {
        return assign(rhs.data);
    }

    foo& operator=(foo&& rhs)
    {
        return assign(std::move(rhs.data));
    }

private:
    T data;
};

int main()
{
    foo<int> x,y;
    const foo<int>& cy = y;

    foo<short> z, w;
    x = y;
    x = cy;
    x = std::move(y);
    x = z;
    x = std::move(w);
    return 0;
}

Edit1: added Member and call to swap.

Edit2: changed assign to take T by value instead of foo<T> added move assignment

NOTE: This now uses the conversion constructor of T when assigning a foo of different type (U)

kiloalphaindia
  • 551
  • 3
  • 6
  • There is no copy and swap here. Also the point is precisely to avoid having to define these functions. Of course defining them myself, is a way to do it. But it leads to code duplication, read the copy and swap idiom link, at the beginning of my message. – user2346536 May 27 '18 at 11:28
  • You must implement the `swap` for each member separately now, because `std::swap(...)` uses the assignment operator causing an infinite recursion. – kiloalphaindia May 27 '18 at 11:36
  • you really may want to read the copy-and-swap link. Your answer does not even implement the necessary "operator=".. http://en.cppreference.com/w/cpp/language/copy_assignment (check signature 1&2 comments) https://stackoverflow.com/questions/3279543/what-is-the-copy-and-swap-idiom – user2346536 May 27 '18 at 11:39
  • also "foo& operator=(foo& rhs)" is a better match than "foo& operator=(const foo& rhs)" ... Try it. so the latter is definitely not an exact match! – user2346536 May 27 '18 at 11:54
  • True, I'll fix this – kiloalphaindia May 27 '18 at 11:57
  • This is not working, actually I did not check carefully enough, arguments are not forwarded correctly, parameters are never passed by lvalue reference even when using std::forward. – user2346536 May 28 '18 at 06:02
  • I edited the question with the actual answer. You may want to copy paste it before I accept you answer because I can't accept an incorrect answer. The main problem in you case was the lack of perfect forwarding. – user2346536 May 28 '18 at 08:10
  • Accepted, with the move. Though now, we finaly implemented most overloads (even so they only call the same functions)... I'm not sure I can be avoided anyway. – user2346536 May 31 '18 at 13:59