3

I'm trying to use rule of 4 1/2 move semantics and remove duplication from the process using CRTP. This has proved difficult as, despite compiling, the following code ends up segfaulting when I try to access members of the derived class after using the assignment operator. Why does this happen and is there a way around this?

Base CRTP Class

// BaseCRTP.h

template<class Derived>
class BaseCRTP {
public:
    BaseCRTP() {};

    BaseCRTP(const BaseCRTP &rhs) {
        static_cast<Derived *>(this)->setCore(static_cast<const Derived&>(rhs));
    };

    BaseCRTP(BaseCRTP &&rhs) {
        static_cast<Derived *>(this)->swap(rhs);
    }

    Derived &operator=(BaseCRTP rhs){
        static_cast<Derived *>(this)->swap(rhs);
        Derived& d = *static_cast<Derived*>(this); // debugger shows d now has the correct value for m_member, no issue here
        return *static_cast<Derived*>(this); // something happens here that causes issue? 
    }
};

Derived Class

// Derived.h

#include "Base.h"
#include <algorithm>

class Derived : public BaseCRTP<Derived>{
private:
    int m_member1;
public:
    using BaseCRTP<Derived>::BaseCRTP;
    using BaseCRTP<Derived>::operator=;

    Derived(int member);
    void setCore(const Derived& rhs);
    void swap(BaseCRTP<Derived> & rhs);
    int getMember() const;
};
// Derived.cpp

#include "Derived.h"

void Derived::setCore(const Derived &rhs) {
    m_member1 = rhs.m_member1;
}

void Derived::swap(BaseCRTP<Derived> &rhs) {
    Derived& rhs_p = static_cast<Derived&>(rhs);
    std::swap(m_member1, rhs_p.m_member1); // members have correct values in debugger
}

Derived::Derived(int member) {
    m_member1 = member;
}

int Derived::getMember() const{
    return m_member1;
}

Main

// main.cpp

#include <iostream>
#include "Derived.h"

int main() {
    Derived d(1);
    int z = d.getMember(); // works fine
    Derived dd(34);
    int w = dd.getMember(); // works fine
    d = dd; // after this d and dd no longer have m_member1 values
    int y =  dd.getMember();  //segmentation fault
    int x =  d.getMember();   // when swapped this also segmentation faults
    std::cout << z << w << y << x << std::endl;
    return 0;
}

UPDATE:

I originally changed void swap(BaseCRTP<Derived> & rhs); to use the parent class as it didn't compile with out doing so, and the debugger seemed to indicate that members were maintained. I've attempted to switch it back with no luck, the function now reads:

void Derived::swap(Derived &rhs) {
    std::swap(m_member1, rhs_p.m_member1);
}

with Derived &operator=(BaseCRTP rhs) now being: Derived &operator=(Derived rhs).

This results in the following compile time errors:

PATH\main.cpp: In function 'int main()':
PATH\main.cpp:9:9: error: ambiguous overload for 'operator=' (operand types are 'Derived' and 'Derived')
     d = dd;
         ^~
In file included from PATH\Derived.h:7:0,
                 from PATH\main.cpp:2:
PATH\Base.h:14:7: note: candidate: constexpr BaseCRTP<Derived>& BaseCRTP<Derived>::operator=(const BaseCRTP<Derived>&) <deleted>
 class BaseCRTP {
       ^~~~~~~~
PATH\Base.h:28:14: note: candidate: Derived& BaseCRTP<Derived>::operator=(Derived) [with Derived = Derived]
     Derived &operator=(Derived rhs){
              ^~~~~~~~
In file included from PATH\main.cpp:2:0:
PATH\Derived.h:10:7: note: candidate: Derived& Derived::operator=(const Derived&) <deleted>
 class Derived : public BaseCRTP<Derived>{
       ^~~~~~~

Apparently deleted members still get to participate in overload resolution ... which is a bit annoying to say the least. surely there must be some way around this? its very clear that the only valid operator is the operator= I've defined as its the only non deleted operator.

UPDATE 2

Sure enough, this whole thing works if I both change the signature AND stop it from being an assingment operator. If I use the following function instead:

Derived& assignmentOperator(Derived rhs){
    static_cast<Derived *>(this)->swap(rhs);
    Derived& d = *static_cast<Derived*>(this); // debugger shows d now has the correct value for m_member, no issue here
    return *static_cast<Derived*>(this); // something happens here that causes issue?
}

and in side main.cpp do:

d.assignmentOperator(dd); 

everything works, there are no compile errors, see the selected answer for why my original seg-faulted. I'll be posting a new question to figure out how I can get around these nasty semantics...

Krupip
  • 4,404
  • 2
  • 32
  • 54
  • 1
    why do you swap in an assignment operator? am I missing something? – 463035818_is_not_an_ai Jun 16 '17 at 14:25
  • Sounds like a slicing problem, or UB due to using moved values after moving for me (didn't spot where it occurs though). – πάντα ῥεῖ Jun 16 '17 at 14:26
  • 1
    @tobi303 Rule of 4 1/2, you swap so that you can deal with both rhs values and lhs values with out having to make two assignment operators. See [official documentation](http://en.cppreference.com/w/cpp/language/rule_of_three) and this [example](https://blog.feabhas.com/2015/01/the-rule-of-the-big-four-and-a-half-move-semantics-and-resource-management/) – Krupip Jun 16 '17 at 14:28
  • @snb thats new to me, will have to read it, but then shouldnt OPs assinment operator be `Derived &operator=(BaseCRTP&& rhs){` instead of `Derived &operator=(BaseCRTP rhs){` ? Whats the point of swapping, when `rhs` is passed by value ?!? – 463035818_is_not_an_ai Jun 16 '17 at 14:31
  • @tobi303 When the passed in value is LHS, it needs to be copied anyway, and that copy is done upon pass by value. When the value is RHS, there is no need to copy, and it is already a RHS value since it is pass by value. You swap normally to deal with more complicated members, I do it here to reduce duplication. When the function ends the pass by value parameters go out of scope and the destructor is called regardless if the values were LHS or RHS originally, since the values were swapped now your old values are being destroyed, you no longer manage clean up in the assignment operator. – Krupip Jun 16 '17 at 14:36
  • @tobi303 If I made the parameter pass by RHS value, I couldn't put LHS values as parameter and I would need yet another assignment operator. – Krupip Jun 16 '17 at 14:38
  • @snb haha didnt realize for my last comment that you are the OP :P. Thanks for your explanations. I will really have to read first (instead of asking stupid questions in comments ;) – 463035818_is_not_an_ai Jun 16 '17 at 14:38

1 Answers1

2
Derived &operator=(BaseCRTP rhs)

This assignment operator slices rhs to the type BaseCRTP<Derived> when the original argument's type was Derived. That's where the members are "lost."

You can make the operator accept BaseCRTP && and instead of using it in Derived, implement operator= there:

Derived& Derived::operator= (Derived rhs)
{
  return BaseCRTP::operator= (std::move(rhs));
}
Angew is no longer proud of SO
  • 167,307
  • 17
  • 350
  • 455
  • I wanted to implement your first suggestion, however I get `"error: ambiguous overload for 'operator=' (operand types are 'Derived' and 'Derived') d = dd;".` I changed Derived's swap accept `Derived` instead of `BaseCRTP` and then changed the operator to accept `Derived rhs` `instead of BaseCRTP` and that was the error. – Krupip Jun 16 '17 at 14:46
  • Also I'm confused as to how the members are lost at the point you reference when casting rhs into Derived seems to preserve members in debugger? – Krupip Jun 16 '17 at 14:47
  • @snb A debugger can only reliably work on legal code. *Undefined Behaviour is Undefined,* and "looks OK in the debugger without being such in reality" is a perfectly valid Undefined Behaviour (as is literally anything else). – Angew is no longer proud of SO Jun 16 '17 at 15:08
  • @snb If option 1 doesn't work, can you try #2? I wasn't 100% sure with option 1, tbh. I'll just remove it from the answer/ – Angew is no longer proud of SO Jun 16 '17 at 15:10
  • I feel like option 1 *should* work, the only reason it isn't is because deleted members can participate in overload resolution, I'm not sure how to get past that. Option 2 is not an option because I end up having to write more code per = operator, and increases code duplication. – Krupip Jun 16 '17 at 15:11
  • Its the fact that I have this same pattern over a very large amount of classes. Additionally It would actually be better for me to just write the assignment operator for each case then with the swap instead of delegating to the CRTP parent. – Krupip Jun 16 '17 at 15:19
  • Ok, your first solution *does work* (sort of), its just a completely separate problem that causes it to fail with the assignment operator; any other arbitrary function definition works for it. You answered my question why the other thing was failing. I added my proposed solution to my post. – Krupip Jun 16 '17 at 15:47