0

I am studying the Rule of Five and it's cousins (Rule of Four and 1/2, Copy and Swap idiom, Friend Swap Function).

I implemented the Rule of Four and 1/2 on a test class. It compiles well. Is there any hidden mistake in my implementation?

I am particulary preoccupied about the unique_ptrs stored in the m_unorederd_map property that I moved in the copy constructor as they can't be copied. Is this the proper way to deal with unique_ptrs in classes?

someclass.h

#ifndef SOMECLASS_H
#define SOMECLASS_H

#include "someotherclass.h"

#include <QString>
#include <QStringList>

#include <memory>
#include <string>
#include <unordered_map>

class SomeClass
{
    QString m_qstring;
    std::unordered_map<std::string, std::unique_ptr<SomeOtherClass>> m_unordered_map;
    int m_int;
    std::string m_string;
    QStringList m_qstringlist;

public:
    SomeClass() = default;

    // Rule of 5 (or Rule of 4 and 1/2)
    // From https://stackoverflow.com/questions/3279543/what-is-the-copy-and-swap-idiom#3279550
    ~SomeClass() = default;                               // Destructor
    SomeClass(SomeClass &other);                          // Copy constructor
    SomeClass(SomeClass &&other);                         // Move constructor
    SomeClass &operator=(SomeClass other);                // Copy/Move assignment operator
    friend void swap(SomeClass &first, SomeClass &second) // Friend swap function
    {
        using std::swap;
        first.m_qstring.swap(second.m_qstring);
        first.m_unordered_map.swap(second.m_unordered_map);
        swap(first.m_int, second.m_int);
        swap(first.m_string, second.m_string);
        first.m_qstringlist.swap(second.m_qstringlist);
    }
};

#endif // SOMECLASS_H

someclass.cpp

#include "someclass.h"

// Copy constructor
SomeClass::SomeClass(SomeClass &other)
    : m_qstring(other.m_qstring),
      m_int(other.m_int),
      m_string(other.m_string),
      m_qstringlist(other.m_qstringlist)
{
    // m_unordered_map holds unique_ptrs which can't be copied.
    // So we move it.
    m_unordered_map = std::move(other.m_unordered_map);
}

// Move constructor
SomeClass::SomeClass(SomeClass &&other)
    : SomeClass()
{
    swap(*this, other);
}

// Copy/Move assignment operator
SomeClass &SomeClass::operator=(SomeClass other)
{
    swap(*this, other);
    return *this;
}
Rsevero
  • 157
  • 2
  • 11
  • Is Qt really important for this? I suspect that it's not and that you could remove it for the purpose of making the question clearer. – Ted Lyngmo Jan 29 '22 at 18:35
  • 1
    Whether the implementation is correct or not aside: There is no reason not to follow the rule-of-zero here as far as I can see. `~SomeClass() = default;` is a good give-away for this. – user17732522 Jan 29 '22 at 18:36
  • 1
    A copy constructor that _moves_ from `other` is not very user-friendly. – Paul Sanders Jan 29 '22 at 18:37
  • *It compiles well* -- Usually new posters/programmers make this unnecessary comment. "Compiling well" means absolutely nothing except that there are no syntax errors. Whether the program runs correctly has nothing to do with whether the program "compiles well". – PaulMcKenzie Jan 29 '22 at 18:40
  • @TedLyngmo Qt is important because I will need to apply this on classes that use several Qt classes like QString, QStringList and others. – Rsevero Jan 29 '22 at 18:41
  • 1
    @Rsevero -- If those Qt classes already have correct copy semantics, all you're doing is putting your code at risk of bugs by adding unnecessary copy functions. Someone adds another member variable, you then have to change your code to remember to handle that member variable. Now someone adds 10 member variables, and only remembers to handle 9 of them -- bug. – PaulMcKenzie Jan 29 '22 at 18:42
  • @user17732522 I believe I need to apply the Rule of Five (or Four and 1/2) because of the Qt members (QString, QStringList, etc) AND because of the unordered_map with unique_ptrs. Or am I wrong? – Rsevero Jan 29 '22 at 18:43
  • @PaulSanders I know but is there another (better) option for a class that has unique_ptrs stored inside it? – Rsevero Jan 29 '22 at 18:44
  • @PaulMcKenzie I know. That's why I posted this question. – Rsevero Jan 29 '22 at 18:45
  • @Rsevero As long as the member types follow normal C++ conventions, you usually only _need_ to deviate from the rule-of-zero if there is some work that needs to be done in the destructor (e.g. because you manually allocated memory, need to close some resource, etc.). I am not very knowledgeable about qt, but I assume that the types follow the usual conventions. – user17732522 Jan 29 '22 at 18:47
  • @PaulMcKenzie I understand your reasoning about the Qt classes but what about the unordered_map with unique_ptrs? – Rsevero Jan 29 '22 at 18:47
  • @Rsevero -- Read the comment I made about the other members and opening up the possibility of bugs maintaining them. Maybe in this case, it would be safer to use `std::shared_ptr` in the map, thus lessening any chance of bugs to happen due to copy/paste errors or just plain tired eyes having to maintain all of those member variables. – PaulMcKenzie Jan 29 '22 at 18:50
  • @Rsevero Maybe you should clarify whether you are deviating from the rule-of-zero only because you think you _have_ to in order for the class to work correctly, or whether you are doing it in order to define a copy operation which deep copies the map and the objects in it (which your current implementation doesn't do though)? (With the rule-of-zero it will be non-copyable, only movable.) – user17732522 Jan 29 '22 at 18:50
  • @user17732522 I am deviating from the rule-of-zero because I thought I would need it to make it moveable. – Rsevero Jan 29 '22 at 18:56
  • @Rsevero -- In addition to the possibility of introducing bugs, even if you handled all of the members correctly by yourself, the code you produced would have two flaws: 1) The traits of the class radically changes by adding your own copy operations, thus possibly stopping the compiler from generating more optimized code, and 2) Once the compiler sees that you've stepped in and added those functions, the compiler will no longer generate the defaults (which are more than likely more optimized than your hand-made code), thus you're thwarting another attempt by the compiler to optimize the code. – PaulMcKenzie Jan 29 '22 at 19:18

1 Answers1

6

Most important of all:

  • This class doesn't need custom copy/move operations nor the destructor, so rule of 0 should be followed.

Other things:

  • I don't like swap(*this, other); in the move ctor. It forces members to be default-constructed and then assigned. A better alternative would be to use a member initializer list, with std::exchange.

    If initializing all members gets tedious, wrap them in a structure. It makes writing the swap easier too.

  • Copy constructor must take the parameter by a const reference.

  • unique_ptrs which can't be copied. So we move it. is a bad rationale. If your members can't be copied, don't define copy operations. In presence of custom move operations, the copy operations will not be generated automatically

  • Move operations (including the by-value assignment) should be noexcept, because standard containers won't use them otherwise in some scenarios.

  • SomeClass() = default; causes members that are normally uninitialized (int m_int;) to sometimes be zeroed, depending on how the class is constructed. (E.g. SomeClass x{}; zeroes it, but SomeClass x; doesn't.)

    Unless you want this behavior, the constructor should be replaced with SomeClass() {}, and m_int should probably be zeroed (in class body).

HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
  • Even the unordered_map with unique_ptrs won't demand custom move operations? std::move (or swap in case of Rule 4 and 1/2) or something similar will be automatically included by the compiler? – Rsevero Jan 29 '22 at 18:52
  • @Rsevero Yes! By default, the compiler performs member-wise operations (default move constructor calls move constructors of all members, and so on). – HolyBlackCat Jan 29 '22 at 18:54