Don't set, just swap! (C++)

> Coding, hacking, computer graphics, game dev, and such...
User avatar
fips
Site Admin
Posts: 170
Joined: Wed Nov 12, 2008 9:49 pm
Location: Prague
Contact:

Don't set, just swap! (C++)

Post by fips »

There's still a bunch of people around who just love bringing tons of setters(), getters() and resetters() to type interfaces. It's certainly welcomed by the type users, but it brings hell to their maintainers, mainly due to the growing number of unmanageable execution paths that ultimately transform the implementation into spaghetti code. I call that types just containers as they are no longer able to effectively preserve their invariants.

Ideally, a very limited number of (re)initialization paths should exist within a type. Two of them are obvious: the constructor and the 'operator ='. Both are responsible for establishing object's invariants. The third one, not so obvious, is just a plane swap() function that can help us to get rid of all the setters() and resetters() by reusing the well established path of the constructor as demonstrates the example blow:

VIEW THE CODE BELOW IN FULL-SCREEN (swap_sample.cpp)

Code: Select all

/*
(c) 2012 +++ Filip Stoklas, aka FipS, http://www.4FipS.com +++
THIS CODE IS FREE - LICENSED UNDER THE MIT LICENSE
ARTICLE URL: http://forums.4fips.com/viewtopic.php?f=3&t=725
*/

#include <string>
#include <algorithm>
#include <cassert>
#include <iostream>

class Person
{
 public:

    Person(const std::string &first_name, const std::string &last_name, int age):
    _first_name(first_name), _last_name(last_name), _age(age)
    {
        // in general here might come quite a complex initialization, it would
        // be error prone to duplicate the logic in setters() and resetters()
        assert(!_first_name.empty() && !_last_name.empty());
        _initials = std::string(1, _first_name[0]) + std::string(1, _last_name[0]);
        assert(!std::any_of(_initials.begin(), _initials.end(), ::islower));
        _full_info = _first_name + " " + _last_name + ", aka " + _initials;
    }

    // just keep swap() in sync with the constructors and 'operator =', that's all!
    void swap(Person &other)
    {
        std::swap(_first_name, other._first_name);        
        std::swap(_last_name, other._last_name);
        std::swap(_age, other._age);
        std::swap(_initials, other._initials);
        std::swap(_full_info, other._full_info);
    }

    // just a bunch of getters, there's no need for setters() nor resetters() !!!
    const std::string & first_name() const { return _first_name; }
    const std::string & last_name() const { return _last_name; }
    int age() const { return _age; }

    // more getters...
    const std::string & full_info() { return _full_info; }

 private:

    std::string _first_name;
    std::string _last_name;
    int _age;

    // in general here come various derived or cached data...
    std::string _initials;
    std::string _full_info;
};

int main()
{
    Person p("Jonh", "Doe", 25);
    std::cout << p.full_info() << " (at " << &p << ")\n";

    // here comes the magic, set the last name by swapping the whole object
    Person(p.first_name(), "Foo", p.age()).swap(p);
    std::cout << p.full_info() << " (at " << &p << ")\n";

    // a full reset is also trivial, note that all the swaps() preserve
    // object's identity (address)
    Person("Jane", "Roe", 18).swap(p);
    std::cout << p.full_info() << " (at " << &p << ")\n";

    return 0;
}

// output:
// Jonh Doe, aka JD (at 002BF820)
// Jonh Foo, aka JF (at 002BF820)
// Jane Roe, aka JR (at 002BF820)
Please note that the whole point of the 'swap' technique is to preserve object's identity (address), otherwise we could just wrap the type to a std:unique_ptr or use another type of handle, and completely recreate the object each time a state update is needed.

It's also worth mentioning that the overhead of swap() is usually pretty insignificant, especially with the move semantics that C++11 brings.