Contents

C++ quick tips: What are conditional special member functions in C++20?

Introduction of concepts in c++20 brought along a new set of problems requiring solving related to wrapper types (like e.g. std::optional, std::variant).

Specifically, special functions (like constructors, copy constructors, destructors etc) need to have the same type of traits as the type they are wrapping i.e. these have to be copyable if the underlying type is copyable, trivially copyable if the underlying types are trivially copyable, and trivially destructible if the underlying types are trivially destructible.

That led to an introduction of a, not so well known feature in c++20, that allows special function overloading.

Template type specialisation and special functions overloading

Consider this example code:

 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
template <typename RetT>
class Chain {
public:
    using FunctorT = std::move_only_function<RetT()>;

    explicit Chain(FunctorT f) :
        f_{std::move(f)}
    {
    }

    template <typename F>
    RetT operator()(F f) {
        return f_(f());
    }

private:
    FunctorT f_;
};

template <>
class Chain<void> {
public:
    using FunctorT = std::move_only_function<void()>;

    explicit Chain(FunctorT f) :
        f_{std::move(f)}
    {
    }

    template <typename F>
    void operator()(F f) {
        f_(f());
    }

private:
    FunctorT f_;
};

I could extract the constructor and the data member to a common base to minimise duplication but with C++20 there’s a better way:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
template <typename RetT>
class Chain {
public:
    using FunctorT = std::move_only_function<RetT()>;

    explicit Chain(FunctorT f) :
        f_{std::move(f)}
    {
    }

    template <typename F>
    RetT operator()(F f) {
        return f_(f());
    }

    template <typename F>
    void operator()(F f) requires std::is_void_v<RetT> {
        f_(f());
    }    

private:
    FunctorT f_;
};

But that’s not all. Type constraints and function overloading can be applied to special functions as well!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
template <typename T>
class X {
public:
    // ctor for when T is an integral type
    X() requires std::is_integral_v<T> = default;

    // dtor for when T is an integral type
    ~X() requires std::is_integral_v<T> {}

    // ctor for when T is a pointer type
    X() requires std::is_pointer_v<T> {}

    // dtor for when T is a pointer type
    ~X() requires std::is_pointer_v<T> {}
};

This feature was introduced with p0848 (later revised as p0847r3) and allows solving problems with std::optional and friends - the underlying wrapped type’s special function traits are now easily propagated to wrappers without excess class template specialisations.

New wording

The new revision introduces an important term prospective destructor and prospective special member function.

In simplest terms, in my understanding, any prospective special member function (including a destructor) is a non-deleted function with defined and satisfied constraints. In case of destructors, there’s an extra stipulation that there can be only one eligible destructor with satisfied constraints.

Exact definition for each function type is described in the rev 1 of the proposal.