# Tillämpar du Rule-of-Zero i C++?

26 september 2025 | https://www.ribomation.se/blog/2025/tillampar-du-rule-of-zero-i-c++/

Rule-of-Zero (RoZ) är ett kodnings-idiom i C++ där man tar vara på språkets/kompilatorns möjligheter att skriva kod åt dig. För att förstå RoZ, måste man först förstå _Rule of 3/5+1_. För att i sin tur, förstå detta behöver man förstå begreppet _special members_ i C++. Samt, för att förstå dessa måste man känna till när de genereras, hur de ser ut och när de anropas. Allt detta går jag igenom steg för steg i denna artikel. Häng med till C++ bakgård.

Special Members
---------------

Visste du att kompilatorn kan generera vissa konstruktorer plus en dela annat också? Dessa medlemsfunktioner kallas för _special members_ och utgörs av följande lista:

Form

Name

Purpose

`T::T()`

Default Constructor

Creates an object with default-initialized members

`T::~T()`

Destructor

Cleans up resources when object is destroyed

`T::T(const T&)`

Copy Constructor

Creates new object as copy of existing object

`T& T::operator=(const T&)`

Copy Assignment Operator

Assigns values from one object to another

`T::T(T&&)`

Move Constructor

Transfers resources from one object to new object

`T& T::operator=(T&&)`

Move Assignment Operator

Transfers resources between existing objects

### Default Constructor - `T::T()`

Denna används för att initiera ett objekt utan objekt-externa data och anropas i följande fall:

*   Argument-less construction
*   Argument-less member object
*   Argument-less super-class constructor invocation
*   Array element initialisation

Här ger jag ett kod-exempel på varje fall, där default-konstruktorn anropas:

```cpp
auto p = Person{};
```

```cpp
class Person {
  std::string name{};
  //...
};
```

```cpp
class Person { /*...*/ };

class Student : public Person {
 public:
  Student(/*...*/) { /*...*/ }
  // ...
};
```

```cpp
Person arr1[5];
auto*  arr2 = new Person[5]; 
```

Om det inte finns en annan konstruktor, så kommer kompilatorn generera en för dig. Innehållet i denna kommer vara tom, så när på default-initiering av eventuella medlems-objekt och super-klasser.

#### Fallgrop

Det finns en fallgrop att vara medveten om. Säg att du börjar enkelt med en klass, skriver den utan konstruktorer inledningsvis och skapar en array. Det kommer att fungera. I ett senare läge bestämmer sig du eller en annan gruppmedlem att lägga till en konstruktor och plötsligt blir det kompileringsfel. Här kommer två kod-snuttar; den första visar ett fungerade program och den andra visar ett kompileringsfel.

```cpp
struct Person {
    inline static int next_id = 1;
    string    name = "person-"s + std::to_string(next_id++);
    unsigned  age = 10 * next_id;
    auto to_string() const {
        return std::format("Person({}, {})", name, age);
    }
};

void run() {
    auto nisse = Person{"Nisse Hult", 42};
    std::cout << nisse.to_string() << "\n";

    Person family[3];
    for (auto&& p : family) std::cout << p.to_string() << "\n";
}

//Output:
Person(Nisse Hult, 42)
Person(person-1, 20)
Person(person-2, 30)
Person(person-3, 40)
```

```cpp
struct Person {
    //...
    Person(string const& name_, unsigned age_) : name{name_}, age{age_} {}
    //...
};

// Compiler error:
error: no matching function for call to ‘usecase2::Person::Person()’
   |          Person family[3];
   |                         ^
```

#### Rule +1

Det är i de flesta fall rekommendabelt att se till att det finns en default-konstruktor. Som minimum, kan man be kompilatorn att generera en när man har vanliga konstruktorer.

```cpp
struct Person {
    //...
    Person(string const& name_, unsigned age_) : name{name_}, age{age_} {}
    Person() = default;
    //...
};
```

### Destructor - `T::~T()`

Denna används för att frigöra allokerade resurser, såsom ett heap-allokerat minnesblock, stänga en fil eller TCP socket, vänta på terminering av en thread och mer därtill. Den anropas i följande fall:

*   Returns from a function
*   Exits an inner block
*   Deletes a dynamic object
*   Disposes an array of objects
*   Throws an exception

Det finns alltid en destruktor till en klass, det kan vara din eller kompilators version. Om det är den senare så blir det en tom funktions-kropp, förutom anrop av destruktorn för eventuella medlems-objekt och super-klasser.

#### Fallgrop

Destruktorns uppgift är att frigöra bland annat allokerade minnesblock. Här kommer ett belysande exempel. Först en version av en klass, med minnesläckage och sedan en ny version utan läckage.

```cpp
namespace usecase1 {
    class Person {
        char*  name;
        unsigned age;
    public:
        Person(const char* name_, unsigned age_) {
            name = ::strcpy(new char[::strlen(name_) + 1], name_);
            age = age_;
        }
        auto to_string() const {
            return std::format("Person({}, {})", name, age);
        }
    };

    void run() {
        std::cout << "-- Use Case 1 --\n";
        auto anna = Person{"Anna Conda", 42};
        std::cout << "anna: " << anna.to_string() << "\n";
    }
}
int main() {
    usecase1::run();
    // usecase2::run();
}
```

```shell
C++> g++ -std=c++23 -Wall -g ../destructor.cxx -o destructor 
C++> valgrind --leak-check=yes -q  ./destructor 
-- Use Case 1 --
anna: Person(Anna Conda, 42)
==67493== 11 bytes in 1 blocks are definitely lost in loss record 1 of 1
==67493==    at 0x48785F3: operator new[](unsigned long) (vg_replace_malloc.c:729)
==67493==    by 0x4005C3E: usecase1::Person::Person(char const*, unsigned int) (destructor.cxx:11)
==67493==    by 0x40048B8: usecase1::run() (destructor.cxx:21)
==67493==    by 0x4004A91: main (destructor.cxx:51)
==67493== 
C++> 
```

Självklart saknas det en destruktor. I usecase 2, har jag lagt till en dylik. Här visar jag de modifierade raderna.

```cpp
namespace usecase2 {
    class Person {
        //...
        ~Person() {
            delete [] name;
        }
        //...
    };
    //...
}

int main() {
    // usecase1::run();
    usecase2::run();
}
```

```shell
C++> g++ -std=c++23 -Wall -g ../destructor.cxx -o destructor 
C++> valgrind --leak-check=yes -q  ./destructor 
-- Use Case 2 --
anna: Person(Anna Conda, 42)
C++>
```

### Copy Constructor - `T::T(const T&)`

Denna används för att skapa en komplett kopia av ett objekt, vilket behövs vid värde-anrop (_call-by-value_) av en funktion. Alternativet är referens-anrop (_call-by-reference_). Den anropas i följande fall:

*   Explicit initialisation from another object
*   Call-by-value
*   Return-by-value

Om du inte skriver en själv kommer kompilatorn generera en dylik med copy-initialisation. Det finns vissa undantag till denna regel, vilket jag återkommer till senare i denna artikel. Så här skulle en sådan kunna se ut.

```cpp
class Person {
    char*  name;
    unsigned  age;
  public:
    //...
    Person(Person const& rhs) : name(rhs.name), age(rhs.age) {}
    //...
};
```

#### Fallgrop

Låt oss bygga vidare på föregående exempel, men denna gång lägga till en funktion som tar ett Person objekt med värdeanrop.

```cpp
namespace usecase1 {
    class Person {
        char*  name;
        unsigned age;
    public:
        Person(const char* name_, unsigned age_) {
            name = ::strcpy(new char[::strlen(name_) + 1], name_);
            age = age_;
        }
        ~Person() {
            delete [] name;
        }
        auto to_string() const {
            return std::format("Person({}, {})", name, age);
        }
    };

    void something(Person p) {
        std::cout << "[something] p: " << p.to_string() << "\n";
    }

    void run() {
        std::cout << "-- Use Case 1 --\n";
        auto anna = Person{"Anna Conda", 42};
        std::cout << "(a) anna: " << anna.to_string() << "\n";
        something(anna);
        std::cout << "(b) anna: " << anna.to_string() << "\n";
    }
}

int main() {
    usecase1::run();
    // usecase2::run();
}
```

```shell
C++> g++ -std=c++23 -Wall -g ../copy-constructor.cxx -o copy-constructor
C++> ./copy-constructor 
-- Use Case 1 --
(a) anna: Person(Anna Conda, 42)
[something] p: Person(Anna Conda, 42)
(b) anna: Person(��P], 42)
free(): double free detected in tcache 2
Aborted
C++> 
```

Hoppsan då, något blev fel i andra utskriften av `anna`. Kan du klura ut vad som är fel?

Eftersom denna klass saknar en copy-constructor, så kommer en genereras där pekarvärdet av name kopieras. I anropet till `something()` kommer parameter p, peka ut samma minnesblock som anna. Så långt verkar det fungera. Problemet uppstår när programmet returnerar från funktionen och destruktorn för p körs, vilken i sin tur tar bort minnesblocket med namnet. I andra utskriften är `name` ett borttaget block, och kvar blir bara skräptecken.

Så om du behöver en destruktor, så behöver du också en kopierings-konstruktor; och vice versa. Här kommer en uppdaterad version av person-klassen.

```cpp
namespace usecase2 {
    class Person {
        //...
        Person(Person const& rhs) {
            name = ::strcpy(new char[::strlen(rhs.name) + 1], rhs.name);
            age  = rhs.age;
        }
        //...
    };
    //...
}
int main() {
    // usecase1::run();
    usecase2::run();
}
```

```shell
C++> g++ -std=c++23 -Wall -g ../copy-constructor.cxx -o copy-constructor
C++> ./copy-constructor 
-- Use Case 2 --
(a) anna: Person(Anna Conda, 42)
[something] p: Person(Anna Conda, 42)
(b) anna: Person(Anna Conda, 42)
C++> 
```

### Copy Assignment Operator - `T& T::operator=(const T&)`

Denna används vid värde-tilldelning av ett objekt till ett annat. Även i detta fall så kommer kompilatorn generera en version om det inte finns en redan. Så här kan en dylik se ut.

```cpp
auto operator =(Person const& rhs) -> Person& {
    name = rhs.name;
    age  = rhs.age;
}
```

#### Fallgrop

Låt oss fortsätta bygga vidare på vår person-klass. Denna gång gör vi en värde-baserad tilldelning (_assignment by-value_).

```cpp
namespace usecase1 {
    class Person {
        char*  name;
        unsigned age;
    public:
        Person() {
            name = nullptr;
            age  = 0;
        }
        Person(const char* name_, unsigned age_) {
            name = ::strcpy(new char[::strlen(name_) + 1], name_);
            age = age_;
        }
        ~Person() {
            delete [] name;
        }
        auto to_string() const {
            return std::format("Person({}, {})", name, age);
        }
    };

    void run() {
        std::cout << "-- Use Case 1 --\n";
        auto anna = Person{"Anna Conda", 42};
        std::cout << "(a) anna: " << anna.to_string() << "\n";
        {
            auto berit = Person{};
            berit = anna;
            std::cout << "berit: " << berit.to_string() << "\n";
        }
        std::cout << "(b) anna : " << anna.to_string() << "\n";
    }
}
int main() {
    usecase1::run();
    // usecase2::run();
}
```

```shell
C++> g++ -std=c++23 -Wall -g ../copy-assignment.cxx -o copy-assignment
C++> ./copy-assignment 
-- Use Case 1 --
(a) anna: Person(Anna Conda, 42)
berit: Person(Anna Conda, 42)
(b) anna : Person(�n=[, 42)
free(): double free detected in tcache 2
Aborted
C++>
```

Hoppsan då, samma problem som vi nyss hade; andra utskriften av anna blev förstörd. Så, tydligen behöver vi också en copy-assignment operator. Så här ser den uppdaterade koden ut.

```cpp
namespace usecase2 {
    class Person {
        //...
        auto operator =(Person const& rhs) -> Person& {
            if (&rhs != this) {
                delete [] name;
                name = ::strcpy(new char[::strlen(rhs.name) + 1], rhs.name);
                age  = rhs.age;
            }
            return *this;
        }
        //...
    };
}
int main() {
    // usecase1::run();
    usecase2::run();
}
```

```shell
C++> g++ -std=c++23 -Wall -g ../copy-assignment.cxx -o copy-assignment
C++> ./copy-assignment 
-- Use Case 1 --
(a) anna: Person(Anna Conda, 42)
berit: Person(Anna Conda, 42)
(b) anna : Person(Anna Conda, 42)
C++>
```

#### Rule of 3

Så om du behöver en av dessa, så behöver de övriga också:

*   `T::~T()`
*   `T::T(T const&)`
*   `auto T::operator =(T const&) -> T&`

### Move Constructor - `T::T(T&&)`

Med C++11 kom en andra form av referenstyp, utöver den klassiska som numera benämns _lvalue reference_ (T&), så finns det också en _rvalue reference_ (T&&). Med en dylik kan man ha en referens till ett transient värde, såsom ett returvärde från en funktion. Vitsen med detta är att kunna trigga en ny form av konstruktor, nämligen _move constructor_ `T::T(T&&)`.

Det här kallas med ett samlingsnamn för _move semantics_ och är en designteknik ämnad att ha både snygg kod och snabb kod (_maintainable, elegant, but also performant_). Den bärande idén är att flytta ägarskapet av interna resurser, såsom minnesblock, från ett objekt till ett annat, i stället för att göra en komplett kopia av dem.

I vissa situationer kan kompilatorn direkt involvera move semantics, medan i andra behöver man vara explicit, genom att anropa `std::move( obj )`.

```cpp
auto obj = SomeType{};
some_function( std::move(obj) );
```

Valet av namn på denna funktion har ifrågasatts, eftersom det är lätt att tro den utför något i run-time. Så är dock inte fallet, utan funktionen ska betraktas som en casting operator, vilken ändrar typ på sitt argument till en rvalue reference. Kikar man på källkoden till denna, så ser innehållet ut ungefär så här.

```cpp
static_cast<T&&>( obj )
```

Även för move-constructor, kan kompilatorn generera en egen version. Dock är det inte lika frikostigt som för copy members. Jag återkommer till detta lite längre fram i artikeln. Ungefär så här kan kompilatorns version se ut.

```cpp
Person(Person&& rhs) noexcept : name{rhs.name}, age{rhs.age} {
    rhs.name = nullptr;
    rhs.age  = 0;
}
```

#### Fallgrop

Vi fortsätter med vår Person-klass. I första versionen har jag markerat copy-members med `delete` och anropar en funktion där argumentet omges med std::move.

```cpp
namespace usecase1 {
    class Person {
        char*  name;
        unsigned age;
    public:
        Person() {
            name = nullptr;
            age  = 0;
        }
        Person(const char* name_, unsigned age_) {
            name = ::strcpy(new char[::strlen(name_) + 1], name_);
            age = age_;
        }
        ~Person() {
            delete [] name;
        }
        Person(Person const&) = delete;
        auto operator =(Person const& rhs) -> Person& = delete;
        auto to_string() const {
            return std::format("Person({}, {})", name, age);
        }
    };

    void something(Person p) {
        std::cout << "[something] p: " << p.to_string() << "\n";
    }

    void run() {
        std::cout << "-- Use Case 1 --\n";
        auto anna = Person{"Anna Conda", 42};
        std::cout << "(a) anna: " << anna.to_string() << "\n";
        something( std::move(anna) );
        std::cout << "(b) anna : " << anna.to_string() << "\n";
    }
}
```

Mycket riktigt så får vi ett kompileringsfel, eftersom copy-constructor är borttagen.

```shell
 error: use of deleted function ‘usecase1::Person::Person(const usecase1::Person&)’
      |         something( std::move(anna) );
      |         ~~~~~~~~~^~~~~~~~~~~~~~~~~~~
```

I det andra fallet har jag lagt till en move-constructor och då kompilerar programmet. Emellertid, har vi flyttat innehållet till parameter `p` och vid returen anropas dess destruktor. Kvar i `run` funktionen finns objektet `anna`, som nu befinner sig i _moved-from state_.

```cpp
namespace usecase2 {
    class Person {
    //...
        Person(Person&& rhs) noexcept {
            name     = rhs.name;
            rhs.name = nullptr;
            age      = rhs.age;
            rhs.age  = 0;
        }
        auto to_string() const {
            return std::format("Person({}, {})", name ? name : "<null>", age);
        }
    //...
    };
```

```shell
-- Use Case 2 --
(a) anna: Person(Anna Conda, 42)
[something] p: Person(Anna Conda, 42)
(b) anna : Person(<null>, 0)
```

### Move Assignment Operator - `T& T::operator=(T&&)`

Analogt med att vi har två copy members, så har vi också två move members. Den sista av special members är då move-assignment operator. Även i detta fall kan kompilatorn generera en version, som kan se ut ungefär så här.

```cpp
auto operator =(Person&& rhs) noexcept -> Person& {
    name = std::move(rhs.name);
    age  = std::move(rhs.age);
}
```

Både name och age är att betrakta som primitiva värden och deras numeriska innehåll kommer att kopieras till vänster-sidan, utan att ändra höger-sidan.

Här går jag direkt till implementationen och när vi kör programmet så får vi ett liknande utfall som för move-constructor.

```cpp
auto operator =(Person&& rhs) noexcept -> Person& {
    if (&rhs != this) {
        delete [] name;
        name = rhs.name;
        rhs.name = nullptr;
        age = rhs.age;
        rhs.age = 0;
    }
    return *this;
}
```

```shell
C++> g++ -std=c++23 -Wall -g ../move-assignment.cxx -o move-assignment
C++> ./move-assignment 
-- Use Case 2 --
(a) anna: Person(Anna Conda, 42)
berit: Person(Anna Conda, 42)
(b) anna : Person(<null>, 0)
C++> 
```

### Rule of Five

I och med C++11, så uppdaterades rule-of-three till rule-of-five. Om du behöver en av dessa fem, så lär du behöva de övriga också.

### Rule of Six

Kombinerar man rule-of-five med rekommendationen att alltid ha en default constructor, så har vi då _Rule-of-Six_.

När genererar kompilatorn en special member?
--------------------------------------------

Howard Hinnant sammanfattade i tabell reglerna för när kompilatorn genererar eller inte gör det, baserat på olika kriterier såsom vilka medlemmar du skriver själv. Denna tabell brukar benämnas _Hinnant table_. Tabellen visades under föredraget "_Everything you need to know about move semantics_", som du kan söka efter på YouTube.

   ![Hinnant Table](/blog/2025/tillampar-du-rule-of-zero-i-c++/hinnant-table-1280w.jpeg "Hinnant Table")

Eftersom move-members kom i C++11, så har man försökt behålla kompatibilitet med classic C++, vilket medför att vi har en liten asymmetri för copy och move members. Som tabellen visar, om man deklarerar en copy-constructor, så försvinner båda move-members. Detta gäller även om man markerar denna som delete.

```cpp
MyClass(MyClass const&) = delete;
```

Rule of Zero
------------

Så, en relevant fråga är ju behöver man verkligen implementera samtliga sex special members? Tja, det beror på valet av medlems variabler. Om du väljer datatyper som redan följer rule-of-five, så behöver du inte implementera någon av dessa alls. Detta kallas för _Rule-of-Zero (RoZ)_.

Tag till exempel vår Person klass. Om vi byter ut datatypen för name till std::string, så kan vi ta bort alla special members. Så här kan det då se ut.

```cpp
class Person {
    string    name{};
    unsigned  age{};
public:
    Person(string name_, unsigned age_) : name{std::move(name_)}, age{age_} {}
    auto to_string() const {
        return std::format("Person({}, {})", name, age);
    }
    auto const& get_name() const { return name; }
    auto get_age() const { return age; }
    auto incr_age() { ++age; return age; }
};
```

Här följer ett demo program som visar både anrop av både copy och move members, vilka då har blivit genererade av kompilatorn.

```cpp
auto something(Person p) {
    p.incr_age();
    cout << "[something] p: " << p.to_string() << "\n";
    return p;
}

int main() {
    auto anna  = Person{"Anna Conda", 42};
    cout << anna.to_string() << "\n";

    auto berit = something(anna);
    cout << "anna : " << anna.to_string() << "\n";
    cout << "berit: " << berit.to_string() << "\n";

    auto carin = berit;
    cout << "berit: " << berit.to_string() << "\n";
    cout << "carin: " << carin.to_string() << "\n";

    auto doris = std::move(carin);
    cout << "carin: " << carin.to_string() << "\n";
    cout << "doris: " << doris.to_string() << "\n";

    auto frida = something( std::move(doris) );
    cout << "doris: " << doris.to_string() << "\n";
    cout << "frida: " << frida.to_string() << "\n";

    cout << "----\n";
    for (auto&& p : {anna, berit, carin, doris, frida})
        cout << p.to_string() << "\n";
}
```

```shell
Person(Anna Conda, 42)
[something] p: Person(Anna Conda, 43)
anna : Person(Anna Conda, 42)
berit: Person(Anna Conda, 43)
berit: Person(Anna Conda, 43)
carin: Person(Anna Conda, 43)
carin: Person(, 43)
doris: Person(Anna Conda, 43)
[something] p: Person(Anna Conda, 44)
doris: Person(, 43)
frida: Person(Anna Conda, 44)
----
Person(Anna Conda, 42)
Person(Anna Conda, 43)
Person(, 43)
Person(, 43)
Person(Anna Conda, 44)
```

Vi kan titta efter i symboltabellen efter de genererade medlemmarna.

```shell
C++> nm --demangle person-roz | grep -i person | cut -d ' ' -f 1,2 --complement | sort -u
Person::incr_age()
Person::~Person()
Person::Person(Person&&)
Person::Person(Person const&)
Person::Person(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, unsigned int)
Person::to_string[abi:cxx11]() const
something(Person)
std::initializer_list<Person>::begin() const
std::initializer_list<Person>::end() const
std::initializer_list<Person>::size() const
```

Tilldelningsoperatorerna har blivit inline funktioner och syns inte i symboltabellen, dessvärre.

När fungerar det _inte_ med Rule-of-Zero?
-----------------------------------------

Tja, det är ju en berättigad fråga och svaret är kort och gott; _när du måste implementera en destruktor_. Då dyker det upp en följdfråga; och när är det då?

När man måste implementera en destruktor, är i följande fall:

1.  I en polymorf basklass, vilken skall vara varken kopierbar eller flyttbar
2.  I en resurshanteringsklass, där resursen ska frigöras i destruktorn

### (1) Polymorf basklass (_Polymorphic Base Class_)

En klass kallas för _polymorf_ om den har minst en virtuell funktion, och därmed är tänkt att fungerar som superklass för en eller flera subklasser. En polymorf basklass, måste också deklarera sin destruktor som virtuell, även om det inte finns någon resurs att frigöra. Det görs enklast genom att markera den med `default`, på följande vis.

```cpp
virtual ~BaseClass() = default;
```

Det typiska sättet att använda en basklass är att deklarera pekare (eller referenser) av denna typ, som sedan pekar på subklass-objekt.

```cpp
Vehicle*  ptr = new Car{...};
ptr->drive(90);
delete ptr;
ptr = new MC{...};
ptr->drive(105);
//...
Shape* shapes[] = {new Rect{...}, new Circle{...}, new Triangle{...}};
for (auto* s : shapes) std::cout << s->area() << "\n";
```

En polymorf basklass, skall varken vara kopierbar eller flyttbar, dvs man ska markera dessa special members med `delete`. Skälet är att förbygga en svårfunnen bug som kallas _slicing_. Här följer ett citat från _C++ Core Guidelines: § C.67_

> If it is accidentally passed by value, with the implicitly generated copy constructor and assignment, we risk slicing: only the base portion of a derived object will be copied, and the polymorphic behavior will be corrupted.

Det mest tydliga är att markera samtliga special members med delete. (I)

```cpp
struct BaseClass {
    //...
    BaseClass(BaseClass const&)     = delete;
    BaseClass(BaseClass&&) noexcept = delete;
    auto operator =(BaseClass const&) -> BaseClass&     = delete;
    auto operator =(BaseClass&&) noexcept -> BaseClass& = delete;
    //...
};
```

Studerar man Hinnant tabellen, så ser man att copy-members också undertrycker generering av move-members. Så det går att korta ned ovanstående till. (II)

   ![Copy members](/blog/2025/tillampar-du-rule-of-zero-i-c++/copy-members-1280w.jpeg "Copy members")

```cpp
struct BaseClass {
    //...
    BaseClass(BaseClass const&)     = delete;
    auto operator =(BaseClass const&) -> BaseClass&     = delete;
    //...
};
```

Kikar man sedan på sista raden i tabellen, framgår att det går att korta ned koden ytterligare till att bara ha en move-assignment operator markerad med delete. (III)

   ![Move assignment](/blog/2025/tillampar-du-rule-of-zero-i-c++/move-assignment-1280w.jpeg "Move assignment")

```cpp
struct BaseClass {
    //...
    auto operator =(BaseClass&&) noexcept -> BaseClass& = delete;
    //...
};
```

Personligen anser jag det första alternativet (I) vara det mest tydliga för läsare av programkoden.

### (2) Resurshanteringsklass (_Resource Management Class_)

En resurshanteringsklass hanterar en _resurs_. En resurs är något som behöver _förvärvas_, _användas_ och sen _frigöras_. Typiska exempel är smarta pekare (t.ex. `std::unique_ptr`), filhanteringsklasser (t.ex. `std::ifstream`) med flera. Konstruktorn förvärvar resursen och destruktorn frigör resursen.

Det finns tre kategorier av resurshanteringsklasser:

1.  Singleton resource
2.  Moveable resource
3.  Cloneable resource

#### (1) Singleton resource

En singleton resource är varken kopierbar eller flyttbar, och vanligtvis begränsad till ett block, varför det inte är meningsfullt att flytta ägarskapet av dess resurs. Typiska exempel är en spårutskriftsklass (_trace class_) och _lock guard_.

```cpp
class Account { 
  std::recursive_mutex lock;  //the resource
  int balance = 0;
 public:
  //...
  int update(int amount) {
    auto t = Trace{"update"};
    auto g = std::lock_guard<std::recursive_mutex>{lock}; //the manager
    balance += amount;
    return balance;
  }
  //...
};
```

#### (2) Moveable resource

En flyttbar resurs (_moveable_) kan inte kopieras men ägarskapet kan flyttas från ett objekt till ett annat. Oftas kan det göras med funktionen `std::move`. Exempel på flyttbar resurs är running thread, open TCP socket, large memory block med flera. Så här kan man modellera en resurshanteringsklass för en flyttbar resurs.

```cpp
#include <optional>

using Resource = ...;

class ResourceManager {
  std::optional<Resource>  resource;
  void drop() noexcept;
 public:
  ResourceManager(Params);
  ResourceManager() = default;
  ~ResourceManager() noexcept;
  ResourceManager(ResourceManager&&) noexcept;
  auto operator=(ResourceManager&&) noexcept -> ResourceManager&;
};

ResourceManager::ResourceManager(Params p) {
    //use the params to acquire the resource
}

ResourceManager::~ResourceManager() noexcept {
  this->drop();
}

ResourceManager::ResourceManager(ResourceManager&& rhs) noexcept 
  : resource{ std::move(rhs.resource) }
{
  rhs.resource.reset();
}

auto ResourceManager::operator=(ResourceManager&& rhs) noexcept -> ResourceManager&
{
  if (this != &rhs) {
    this->drop();
    std::swap(this->resource, rhs.resource);
  }
  return *this;
}

void ResourceManager::drop() noexcept {
  if (resource) {
    try { /*dispose it here*/ }
    catch(...) { /*perhaps do some logging*/ }
    resource.reset();
  }
}
```

#### (3) Cloneable resource

En kopierbar resurs är normalt också en flyttbar resurs, och i detta fall måste man också¨ implementer samtliga sex special members.

Ett komplett (realistiskt) programexempel
-----------------------------------------

Tills sist, tänkte jag visa ett realistiskt exempel i två versionen; den första enligt rule-of-siz och den andra enligt rule-of-zero. Både versionerna används i samma demo-program. Programmet läser och skriver ut metadata från en PNG-fil.

En PNG bildfil har följande layout i början av filen.

```text
PNG file header layout
BE     = Big Endian
uint32 = 32-bit unsigned int
+------------------------+-------------------------------+
| 8 bytes                | PNG signature                 |
+------------------------+-------------------------------+
| 4 bytes (uint32 BE)    | IHDR length (must be 13)      |
+------------------------+-------------------------------+
| 4 bytes                | "IHDR"                        |
+------------------------+-------------------------------+
| 4 bytes (uint32 BE)    | Width                         |
+------------------------+-------------------------------+
| 4 bytes (uint32 BE)    | Height                        |
+------------------------+-------------------------------+
| 1 byte                 | Bit depth                     |
+------------------------+-------------------------------+
| 1 byte                 | Color type                    |
+------------------------+-------------------------------+
| 1 byte                 | Compression method            |
+------------------------+-------------------------------+
| 1 byte                 | Filter method                 |
+------------------------+-------------------------------+
| 1 byte                 | Interlace method              |
+------------------------+-------------------------------+
| 4 bytes (uint32 BE)    | CRC for IHDR                  |
+------------------------+-------------------------------+
```

_PNG signature_ utgörs av filens _magic number_, och har följande innehåll.

```text
0x89,          'P', 'N', 'G', 0x0D, 0x0A, 0x1A,   0x0A
prefix number   image type    CR    LF    Ctrl-Z  LF
```

Här kommer huvudprogrammet. Filen läses in med funktionen `load`, som returnerar en buffert (vilken vi har flera versioner av). Sen valideras filen, dvs kollar att det verkligen är en PNG-fil. Till sist, läses delar av metadata och skrivs ut. Notera den "moderna" versionen av att vända ett heltal i big-endian format till little-endian (`read_bigendian_at`). Vem säger att man inte kan programmera låg-nivå uppgifter med hög-nivå funktioner. 😊

```cpp
#include <iostream>
#include <fstream>
#include <filesystem>
#include <string>
#include <chrono>
#include <array>
#include <span>
#include <algorithm>
#include <concepts>
#include <cstdint>

#include "types.hxx"  // using BYTE = unsigned char;
#include "buffer-rule-6.hxx"
#include "buffer-rule-0.hxx"
#include "buffer-rule-0-uniptr.hxx"

namespace fs = std::filesystem;
namespace cr = std::chrono;

template<typename BufferType>
auto load(fs::path filename) -> BufferType {
    auto file = std::ifstream{filename, std::ios::binary};
    if (!file)
        throw std::invalid_argument{"cannot open file:" + filename.string()};

    auto const file_size = fs::file_size(filename);
    auto buf = BufferType{file_size};
    file.read(reinterpret_cast<char *>(buf.data()), buf.size());
    if (file.gcount() != static_cast<long>(buf.size()))
        throw std::runtime_error{"failed to load whole file"};

    return buf;
}

template<typename BufferType>
bool is_png(BufferType const& buf) noexcept {
    auto constexpr PNG_SIGNATURE = std::array<BYTE, 8>{
        0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A
    };

    if (buf.size() < PNG_SIGNATURE.size()) return false;
    auto prefix = std::span<const BYTE>{buf.data(), PNG_SIGNATURE.size()};

#if __cpp_lib_ranges >= 201911
    return std::ranges::equal(prefix, PNG_SIGNATURE);
#else
    return std::equal(prefix.begin(), prefix.end(), PNG_SIGNATURE.begin(), PNG_SIGNATURE.end());
#endif
}

template<std::size_t NUM_BYTES, std::unsigned_integral UInt>
    requires (sizeof(UInt) == NUM_BYTES)
auto read_bigendian_at(BYTE const* base_address, std::size_t offset) -> UInt {
    auto slice = std::span<const BYTE, NUM_BYTES>(base_address + offset, NUM_BYTES);
    auto bytes = std::array<BYTE, NUM_BYTES>{};

    if constexpr (std::endian::native == std::endian::little) {
        // Big-endian source -> reverse-copy to little-endian host
        std::copy(slice.rbegin(), slice.rend(), bytes.begin());
    } else {
        // Host already in big-endian: normal copy
        std::copy(slice.begin(), slice.end(), bytes.begin());
    }

    return std::bit_cast<UInt>(bytes);
}

template<typename BufferType>
void validate(BufferType const& buf) {
    auto constexpr MIN_SIZE = 8 + 25; //signature + IHDR minimum
    if (not is_png(buf) || buf.size() < MIN_SIZE)
        throw std::invalid_argument{"invalid png: missing prefix or too small size"};

    auto ihdr_length = read_bigendian_at<4, uint32_t>(buf.data(), 8);
    auto type        = reinterpret_cast<const char *>(buf.data() + 12);
    if (ihdr_length != 13 || std::string(type, 4) != "IHDR")
        throw std::invalid_argument{"invalid png: missing IHDR"};
}

template<typename BufferType>
void usecase(std::string const& name, fs::path const& filename) {
    std::cout << "--- Use case: " << name << " ---\n";
    auto start_time = cr::high_resolution_clock::now();

    auto buf = load<BufferType>(filename);
    std::cout << "Loaded " << buf.size() << " bytes from " << filename.string() << "\n";

    validate(buf);
    auto const width  = read_bigendian_at<4, uint32_t>(buf.data(), 16);
    auto const height = read_bigendian_at<4, uint32_t>(buf.data(), 20);
    auto const depth  = static_cast<unsigned>(buf.data()[24]);
    auto const color  = static_cast<unsigned>(buf.data()[25]);
    std::cout << "PNG-metadata: "
            << "dimension="   << width << "x" << height << " px,"
            << " bit depth="  << depth << ", "
            << " color type=" << color << "\n";

    auto end_time = cr::high_resolution_clock::now();
    auto elapsed  = cr::duration_cast<cr::milliseconds>(end_time - start_time);
    std::cout << "Elapsed " << elapsed.count() << " ms\n";
}

int main() {
    auto const filename = fs::path{"generic-article-image.png"};
    usecase<ribomation::rule_of_6::Buffer>("Rule of Six", filename);
    usecase<ribomation::rule_of_0::Buffer>("Rule of Zero", filename);
    usecase<ribomation::rule_of_0_uptr::Buffer>("Rule of Zero (unique_ptr)", filename);
}
```

Utdata från programmet ser ut så här.

```shell
--- Use case: Rule of Six ---
Loaded 1002347 bytes from generic-article-image.png
PNG-metadata: dimension=1300x685 px, bit depth=8,  color type=2
Elapsed 12 ms
--- Use case: Rule of Zero ---
Loaded 1002347 bytes from generic-article-image.png
PNG-metadata: dimension=1300x685 px, bit depth=8,  color type=2
Elapsed 11 ms
--- Use case: Rule of Zero (unique_ptr) ---
Loaded 1002347 bytes from generic-article-image.png
PNG-metadata: dimension=1300x685 px, bit depth=8,  color type=2
Elapsed 11 ms

Process finished with exit code 0
```

### Rule-of-Six version

I denna första version implementerar vi alla sex special members.

```cpp
#pragma once
#include <utility>
#include <cstddef>
#include <cstring>
#include "types.hxx"

namespace ribomation::rule_of_6 {
    class Buffer {
        std::size_t  num_bytes;
        BYTE*        storage;
    public:
        Buffer() noexcept : num_bytes{0}, storage{nullptr} {}
        explicit Buffer(std::size_t n)
            : num_bytes{n}, storage{n > 0UL ? new BYTE[n] : nullptr} {}
        ~Buffer() {
            delete[] storage;
        }

        Buffer(Buffer const& other)
            : num_bytes{other.num_bytes},
              storage{other.num_bytes > 0 ? new BYTE[other.num_bytes] : nullptr} 
        {
            if (storage != nullptr) {
                std::memcpy(storage, other.storage, num_bytes);
            }
        }
        Buffer(Buffer&& other) noexcept
            : num_bytes{other.num_bytes}, storage{other.storage} 
        {
            other.num_bytes = 0;
            other.storage = nullptr;
        }

        Buffer& operator=(Buffer const& other) {
            if (this != &other) {
                delete[] storage;
                num_bytes = other.num_bytes;
                if (num_bytes > 0) {
                    storage = new BYTE[other.num_bytes];
                    std::memcpy(storage, other.storage, num_bytes);
                } else {
                    storage = nullptr;
                }
            }
            return *this;
        }
        Buffer& operator=(Buffer&& other) noexcept {
            if (this != &other) {
                delete[] storage;
                num_bytes = other.num_bytes;
                other.num_bytes = 0;
                storage = other.storage;
                other.storage = nullptr;
            }
            return *this;
        }

        auto        size() const noexcept { return num_bytes; }
        BYTE*       data() noexcept { return storage; }
        BYTE const* data() const noexcept { return storage; }
    };
}
```

### Rule-of-Zero version

I denna andra version, är det remarkabelt hur lite kod det är kvar. Demoprogrammet demonstrerar ju att denna version fungerar exakt lika som den första.

```cpp
#pragma once
#include <vector>
#include "types.hxx"

namespace ribomation::rule_of_0 {
    class Buffer {
        std::vector<BYTE> storage{};
    public:
        Buffer() noexcept = default;
        explicit Buffer(std::size_t n) : storage(n) { }

        auto        size() const noexcept { return storage.size(); }
        BYTE*       data() noexcept { return storage.data(); }
        BYTE const* data() const noexcept { return storage.data(); }
    };
}
```

### Smart Pointer version

Säg att vi önskar implementera klassen med en smart pekare, hur blir det då? Nedanstående version använder `std::unique_ptr` och som konsekvens har vi då en flyttbar men inte kopieringsbar buffert, vilket troligtvis är help ok i detta fall.

```cpp
#pragma once
#include <memory>
#include <cstddef>
#include "types.hxx"

namespace ribomation::rule_of_0_uptr {
    class Buffer {
        std::size_t num_bytes{};
        std::unique_ptr<BYTE[]> storage;
    public:
        Buffer() noexcept = default;
        explicit Buffer(std::size_t n)
            : num_bytes{n}, storage{n ? std::make_unique<BYTE[]>(n) : nullptr} {}

        auto         size() const noexcept -> std::size_t { return num_bytes; }
        BYTE*        data() noexcept { return storage.get(); }
        BYTE  const* data() const noexcept { return storage.get(); }
    };
}
```

Sammanfattning – Takeaways
--------------------------

*   **Rule of Zero** är den moderna standarden: välj datatyper som redan gör jobbet åt dig.
*   Om du själv måste hantera resurser → använd **Rule of Three/Five/Six** för att undvika buggar.
*   Genom att låta kompilatorn generera special members minskar du kodmängden och ökar robustheten.
*   Rule-of-Zero handlar om att frigöra dig från lågnivådetaljer och fokusera på din egen applikationslogik.

Vill du arbeta ännu mer effektivt med modern C++ och undvika fallgropar i dina projekt, så är detta precis vad jag går igenom i mina kurser och workshops. Kontakta mig för en offert.
