Implementing Move Semantics (class String, Live Hacked)

Copy Semantics, Revisited

  • Copy semantics is complicated when resources are maintained (e.g. dynamic memory is allocated)

  • Compiler generated copy semantics is exactly counter-bugfree in such situations

  • pointers are copied, not the resources that they point to

class String, Without Copy

  • class String again

  • Copy explicitly deleted (added again later)

  • ⟶ neither copy nor move (remember: move is generated by compiler only when no dtor or copy is there

  • Implementation

    #ifndef STRING_H
    #define STRING_H
    
    #include <cstring>
    
    class String
    {
    public:
        String() : _c_str(nullptr) {}
        String(const char* s)
        : _c_str(new char[strlen(s)+1])
        {
            strcpy(_c_str, s);
        }
        ~String()
        {
            delete[] _c_str;
        }
    
        String(const String&) = delete;             // <--- delete copy ctor
        String& operator=(const String&) = delete;  // <--- delete copy assignment operator
    
        const char* c_str() const { return _c_str; }
        size_t size() const { return (_c_str == nullptr)? 0 : strlen(_c_str); }
    
    private:
        char* _c_str;
    };
    
    #endif
    
  • Basic functionality/usage (other than copy/move)

    #include <gtest/gtest.h>
    
    #include "string-10.h"
    
    TEST(string_move_suite, basic)
    {
        String s("Hello");
        ASSERT_EQ(s.size(), 5);
        ASSERT_EQ(strcmp(s.c_str(), "Hello"), 0);
    }
    

Return From Function: Copy (Or Move) Wanted

  • Copy explicitly deleted

  • Rules: no move semantics either (we don’t know what that is anyway)

  • Return from function?

#include <gtest/gtest.h>

#include "string-10.h"

static String make_a_string_from(const char* s)
{
    String ret_s(s);
    return ret_s;                           // <--- copy constructor requested but deleted
}

TEST(string_move_suite, bogus)
{
    String s = make_a_string_from("Hello"); // <--- copy constructor requested but deleted
}
  • No move, no copy

  • ⟶ error

code/string-10-suite-bogus.cpp:8:12: error: use of deleted function ‘String::String(const String&)’

Move Constructor? Move?

String make_a_string_from(const char* s)
{
    String ret_s(s);
    return ret_s;      // <--- proven: ret_s not accessible past this point
}
  • Local variable ret_s not accessible anymore after return, obviously

  • No need for a copy altogether which would be

    • Allocate destination (caller’s function value)

    • Copy source (ret_s to destination)

    • Free source (ret_s)

  • ⟶ enter move: transfer source to destination without copying

  • Compiler knows all that

  • ⟶ can insert move constructor to transfer returned value to final location

Implementing Move Constructor

  • Prototype: String(String&& from) noexcept

  • Transfers guts of from into this

  • noexcept is good practice but not required

  • Invalidates from ⟶ resources not deleted twice

  • Attention: invalidated object should still be usable!

#ifndef STRING_H
#define STRING_H

#include <cstring>

class String
{
public:
    String() : _c_str(nullptr) {}
    String(const char* s)
    : _c_str(new char[strlen(s)+1])
    {
        strcpy(_c_str, s);
    }
    ~String()
    {
        delete[] _c_str;
    }
    String(String&& from) noexcept     // <--- move constructor
    : _c_str(from._c_str)              // <--- absorb from into this
    {
        from._c_str = nullptr;         // <--- invalidate from
    }

    String(const String&) = delete;
    String& operator=(const String&) = delete;

    const char* c_str() const { return _c_str; }
    size_t size() const { return (_c_str == nullptr)? 0 : strlen(_c_str); }

private:
    char* _c_str;
};

#endif
#include <gtest/gtest.h>

#include "string-20.h"

static String make_a_string_from(const char* s)
{
    String ret_s(s);
    return ret_s;                              // <--- move-return
}

TEST(string_move_suite, move_return)
{
    String s = make_a_string_from("Hello");    // <--- move-initialization
}

Note

The manual operations (move _c_str from there to here, and invalidated there) can also be written as

#include <utility>

String(String&& from) noexcept
: _c_str(std::exchange(from._c_str, nullptr) {}

And Move Assignment?

#include <gtest/gtest.h>

#include "string-20.h"

static String make_a_string_from(const char* s)
{
    String ret_s(s);
    return ret_s;
}

TEST(string_move_suite, move_assignment)
{
    String s;
    s = make_a_string_from("Hello");   // <--- copy assignment requested but deleted
}
  • (Copy) assignment was deleted

  • No move assignment

  • ⟶ no assigment

code/string-20-suite-bogus.cpp:14:35: error: use of deleted function ‘String& String::operator=(const String&)’

Implementing Move Assignment

  • Prototype: String& operator=(String&&) noexcept

  • Transfers guts, and invalidates source (just like move constructor)

  • Much like copy assignment ⟶ self assignment check

#ifndef STRING_H
#define STRING_H

#include <utility>
#include <cstring>

class String
{
public:
    String() : _c_str(nullptr) {}
    String(const char* s)
    : _c_str(new char[strlen(s)+1])
    {
        strcpy(_c_str, s);
    }
    ~String()
    {
        delete[] _c_str;
    }
    String(String&& from) noexcept
    : _c_str(std::exchange(from._c_str, nullptr)) {}

    String(const String&) = delete;
    String& operator=(const String&) = delete;

    String& operator=(String&& from) noexcept
    {
        if (this != &from) {      // <--- self assignment check
            delete[] _c_str;
            _c_str = std::exchange(from._c_str, nullptr);
        }
        return *this;
    }

    const char* c_str() const { return _c_str; }
    size_t size() const { return (_c_str == nullptr)? 0 : strlen(_c_str); }

private:
    char* _c_str;
};

#endif