PImpl+unique_ptr & defaulted MAO

This is just a helpful tip for anyone using the PImpl idiom with std::unique_ptr and a defaulted move assignment operator in the main class - you can't declare the move assignment operator as default in the header, but you can do it in the implementation.

(This is why: http://home.roadrunner.com/~hinnant/incomplete.html )

Here's an example: (compiled under clang)
Test.hpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#ifndef __Test_HeaderPlusPlus__
#define __Test_HeaderPlusPlus__

#include <memory>

struct Test
{
	Test();
	Test(Test const&) = delete;
	Test &operator=(Test const&) noexcept(true) = delete;
	Test(Test &&) = default; //you can declare this default,
	                                       //but not this (not in the header)
	Test &operator=(Test &&) noexcept(true) /*= default*/;
	~Test() noexcept(true);
private:
	struct Impl;
	std::unique_ptr<Impl> impl;
};

#endif 
Main.cpp
1
2
3
4
5
6
7
#include "Test.hpp"

int main()
{
	Test t1 {}, t2 {};
	t2 = std::move(t1); //for the sake of testing
}
Test.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "Test.hpp"

struct Test::Impl
{
};

Test::Test() : impl(new Impl)
{
}
Test::~Test() noexcept(true)
{
}

//declare it default in the implementation file
Test &Test::operator=(Test &&) noexcept(true) = default;


I just thought I'd post this because I ran into this myself and it wasn't an obvious fix - if I hadn't known you could declare things default outside of the initial declaration, I'd have tried to implement a trivial MAO myself or just have declared it deleted.
Last edited on
You can write this in articles
It wouldn't make sense to have this little snippet by itself - it would go better in an article about the PImpl idiom.
Last edited on
Ideally, the move assignment operator and the destructor should be declared noexcept
@JLBorges I added them in and recompiled - thanks. Should the Move Constructor also be noexcept?
Last edited on
> Should the Move Constructor also be noexcept?

It makes no difference. The move constructor of Test is declared as defaulted; std::unique_ptr<> is nothrow move constructible; therefore the defaulted move constructor of Test is implicitly noexcept

The move assignment operator of Test is not declared as defaulted , and an explict noexcept is required.

Declaring explicitly deleted copy constructor and copy assignment for Test also makes no difference. std::unique_ptr<> is not copy constructible or copy assignable; these would have been implicitly deleted.

In the following snippet, A, B and C are all equivalent as far as the foundation operations are concerned:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include <memory>
#include <type_traits>
#include <iostream>

struct A
{
    A() ;

    // copy constructor and copy assignment are implicitly deleted

    // move constructor is declared as defaulted
    A( A&& ) noexcept = default ;

    A& operator=( A&& ) noexcept ;
    ~A() noexcept ;

    private:
        struct opaque ;
        std::unique_ptr<opaque> implementation ;
};

struct B
{
    B() ;

    // copy constructor and copy assignment are implicitly deleted

    // move constructor is declared as defaulted;
    // the defaulted move constructor is implicitly noexcept
    B( B&& ) = default ;

    B& operator=( B&& ) noexcept ;
    ~B() noexcept ;

    private:
        struct opaque ;
        std::unique_ptr<opaque> implementation ;
};

struct C
{
    C() ;

    // copy constructor and copy assignment are explicity deleted
    C( C& ) = delete ;
    C& operator=( C& ) = delete ;

    // move constructor is declared as defaulted
    C( C&& ) noexcept = default ;

    C& operator=( C&& ) noexcept ;
    ~C() noexcept ;

    private:
        struct opaque ;
        std::unique_ptr<opaque> implementation ;
};

int main()
{
    std::cout << std::boolalpha << "A - \n  "
               << std::is_copy_constructible<A>::value << "\n  " // false
               << std::is_copy_assignable<A>::value << "\n  "  // false
               << std::is_nothrow_move_constructible<A>::value << "\n  " // true
               << std::is_nothrow_move_assignable<A>::value << '\n' ; // true

    std::cout << "\nB - \n  "
               << std::is_copy_constructible<B>::value << "\n  " // false
               << std::is_copy_assignable<B>::value << "\n  " // false
               << std::is_nothrow_move_constructible<B>::value << "\n  " // true
               << std::is_nothrow_move_assignable<B>::value << '\n' ; // true

    std::cout << "\nC - \n  "
               << std::is_copy_constructible<C>::value << "\n  " // false
               << std::is_copy_assignable<C>::value << "\n  " // false
               << std::is_nothrow_move_constructible<C>::value << "\n  " // true
               << std::is_nothrow_move_assignable<C>::value << '\n' ; // true
}

Ah, ok. I like being more explicit though because it's something that could change by changing something else and you wouldn't really realize it.
Last edited on
Topic archived. No new replies allowed.