'Named Parameter Idiom and non-copyable class

I am implementing a class that uses Named Parameter Idiom to initialise its members:

class Person
{
    public:
        Person & first_name(std::string n) { m_first_name = n; return *this; }
        Person & last_name(std::string l) { m_last_name = l; return *this; }
        Person & age(int a) { m_age = a; return *this; }

        std::string get_first_name() const { return m_first_name; }
        std::string get_last_name() const { return m_last_name; }
        int get_age() const { return m_age; }

    private:
        std::string m_first_name;
        std::string m_last_name;
        int m_age;
};

So far, everything works fine.

#include <string>
#include <iostream>

class Person
{
    public:
        Person & first_name(std::string n) { m_first_name = n; return *this; }
        Person & last_name(std::string l) { m_last_name = l; return *this; }
        Person & age(int a) { m_age = a; return *this; }

        std::string get_first_name() const { return m_first_name; }
        std::string get_last_name() const { return m_last_name; }
        int get_age() const { return m_age; }

    private:
        std::string m_first_name;
        std::string m_last_name;
        int m_age;
};

void print(Person const & p)
{
    std::cout << p.get_last_name() << ", " << p.get_first_name() << ", " << p.get_age() << '\n';
}

int main()
{
    auto p1 = Person().first_name("Joe").age(20);
    auto p2 = Person().last_name("Kane").age(50);

    print(p1);
    print(p2);
}

The above code outputs

, Joe, 20
Kane, , 50

as expected.

However, as soon as I make my class non-copyable (in my case by adding a std::unique_ptr member), the above code fails to compile

#include <string>
#include <memory>
#include <iostream>

class Person
{
    public:
        Person & first_name(std::string n) { m_first_name = n; return *this; }
        Person & last_name(std::string l) { m_last_name = l; return *this; }
        Person & age(int a) { m_age = a; return *this; }

        std::string get_first_name() const { return m_first_name; }
        std::string get_last_name() const { return m_last_name; }
        int get_age() const { return m_age; }

    private:
        std::string m_first_name;
        std::string m_last_name;
        int m_age;
        std::unique_ptr<int> m_ptr; // added member
};

void print(Person const & p)
{
    std::cout << p.get_last_name() << ", " << p.get_first_name() << ", " << p.get_age() << '\n';
}

int main()
{
    auto p1 = Person().first_name("Joe").age(20);
    auto p2 = Person().last_name("Kane").age(50);

    print(p1);
    print(p2);
}

with the following error from G++:

prog.cc: In function 'int main()':
prog.cc:30:48: error: use of deleted function 'Person::Person(const Person&)'
   30 |     auto p1 = Person().first_name("Joe").age(20);
      |                                                ^
prog.cc:5:7: note: 'Person::Person(const Person&)' is implicitly deleted because the default definition would be ill-formed:
    5 | class Person
      |       ^~~~~~
prog.cc:5:7: error: use of deleted function 'std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = int; _Dp = std::default_delete<int>]'
In file included from /opt/wandbox/gcc-head/include/c++/10.0.0/memory:82,
                 from prog.cc:2:
/opt/wandbox/gcc-head/include/c++/10.0.0/bits/unique_ptr.h:456:7: note: declared here
  456 |       unique_ptr(const unique_ptr&) = delete;
      |       ^~~~~~~~~~
prog.cc:31:48: error: use of deleted function 'Person::Person(const Person&)'
   31 |     auto p2 = Person().last_name("Kane").age(50);
      |                

I fixed this by returning rvalues:

class Person
{
    public:
        Person && first_name(std::string n) { m_first_name = n; return std::move(*this); }
        Person && last_name(std::string l) { m_last_name = l; return std::move(*this); }
        Person && age(int a) { m_age = a; return std::move(*this); }

        // ...
};

Is this the proper fix? Are there any pitfalls I should consider?



Solution 1:[1]

The problem is that in declaration of p1 and p2

//      V <-- first copy
auto p1 = Person().first_name("Joe").age(20);
auto p2 = Person().last_name("Kane").age(50);
//      ^ <-- second copy

you have two Person copy.

You can solve the problem moving the objects

auto p1 = std::move(Person().first_name("Joe").age(20));
auto p2 = std::move(Person().last_name("Kane").age(50));

or declaring p1 and p2 before

Person p1;
Person p2;

p1.first_name("Joe").age(20);
p2.last_name("Kane").age(50);

Your solution (return Person && from set methods) give the same effect as the first one (use of std::move()) but is very intrusive: every time you set something, you "move" the object.

Solution 2:[2]

first_name, last_name and age return l-value reference, which extends the lifetime of Person(), leading that the expression on the right side was no longer a temporary. (Specifically, a pr-value is converted in to a x-value)

So as @max66 mentioned, a copy-constructor was called instead of a move-constructor.


To deal with it, l-value and r-value should be operated respectively.

With ref-qualifier introduced since C++11:

Person& first_name(std::string n) & { m_first_name = std::move(n); return *this; }
Person&& first_name(std::string n) && { m_first_name = std::move(n); return std::move(*this); }

Person& last_name(std::string l) & { m_last_name = std::move(l); return *this; }
Person&& last_name(std::string l) && { m_last_name = std::move(l); return std::move(*this); }

Person& age(int a) & { m_age = a; return *this; }
Person&& age(int a) && { m_age = a; return std::move(*this); }

Then clients can just write the simple code auto person = Person().first_name("Joe") as what you expected.


With Deducing this introduced in C++23, fluent interface in your example can even be simplified as follows:

template<typename Self>
Self&& first_name(this Self&& self, std::string n) { m_first_name = std::move(n); return std::forward<Self>(self); }

template<typename Self>
Self&& last_name(this Self&& self, std::string l) { m_last_name = std::move(l); return std::forward<Self>(self); }

template<typename Self>
Self&& age(this Self&& self, int a) { m_age = a; return std::forward<Self>(self); }

I've noticed the tag c++17 is linked to the question. Maybe you have thought copy elision would be triggered without any unnecessary copy/move-constructor.

Unfortunately, not only l-value reference would extend the lifetime of a pr-value, r-value reference could also convert it into x-value.

A redundant move-constructor would still be evoked if the compiler were not intelligent enough to omit it out or even there's some side-effects, which may affect the run-time efficiency.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 max66
Solution 2