Home > Enterprise >  How to make a class template copy/move constructible/assignable with `std::enable_if_t`
How to make a class template copy/move constructible/assignable with `std::enable_if_t`

Time:04-18

Context

I'm writing a class template with three parameters so that it is copy/move constructible, copy/move assignable. The class has two template parameters: (1) a type named tree_t and (2) an enumeration value

enum class bounding_t { bound, unbound };

If (2) is bounding_t::bound then the class needs to have a member const tree_t&; when (2) is bounding_t::unbound, the class should not have it. For this I used conditional inheritance:

template <typename tree_t, bounding_t bound>
using base_impl =
    std::conditional_t<is_collection_bound<bound>,
                       base_tree<tree_t>,
                       base<tree_t>>;

template <typename tree_t, bounding_t bound>
class arrangement_collection : public base_impl<tree_t, bound> { }

The constructors use std::enable_if_t to construct the parent class appropriately.

Attempts at solving

I tried several things to make the class copy/move assignable and copy/move constructible (I'm not sure I can list all of them without exceeding body size limit), but I failed to make it so that none of the assertions fail, and so that the compiler does not issue any warnings. The MWE below makes four assertions fail, all involving bounding_t::bound. The only change in the code that makes those four assertions not fail is the definition of the copy assignment operator without template parameters (the definition of this operator= in the MWE below has template parameters).

arrangement_collection& operator= (const arrangement_collection& ac) noexcept
{
    m_num_nodes = ac.m_num_nodes;
    return *this;
}

Without the template parameters, however, I get the following warning that I don't understand:

warning: implicitly-declared ‘constexpr arrangement_collection<free_tree, bounding_t::bound>::arrangement_collection(const arrangement_collection<free_tree, bounding_t::bound>&)’ is deprecated [-Wdeprecated-copy]
../untitled2/main.cpp: In function ‘arrangement_collection<free_tree, bounding_t::bound> proxy_copy()’:
../untitled2/main.cpp:232:29: warning: implicitly-declared ‘constexpr arrangement_collection<free_tree, bounding_t::bound>::arrangement_collection(const arrangement_collection<free_tree, bounding_t::bound>&)’ is deprecated [-Wdeprecated-copy]
  232 |         const auto ac2 = ac.first;
      |                             ^~~~~
../untitled2/main.cpp:122:33: note: because ‘arrangement_collection<free_tree, bounding_t::bound>’ has user-provided ‘arrangement_collection<tree_t, bound>& arrangement_collection<tree_t, bound>::operator=(const arrangement_collection<tree_t, bound>&) [with tree_t = free_tree; bounding_t bound = bounding_t::bound]’
  122 |         arrangement_collection& operator= (const arrangement_collection& ac) noexcept
      |                                 ^~~~~~~~

I'm using g (Ubuntu 11.1.0-1ubuntu1~20.04) 11.1.0 and warning flags -Wall -W.

Question

How can I define the copy/move constructors/assignment operators of this class template so that all assertions succeed without warnings?

Minimal Working Example

The code below contains the class definition, has a few assertions at the end (with std::is_constructible_v<> and many others), and a little main() procedure illustrating a use case.

#include <type_traits>
#include <cinttypes>
#include <utility>

// -----------------------------------------------------------------------------

struct free_tree {
    uint64_t get_num_nodes() const noexcept { return 10; }
    free_tree() noexcept = default;
    free_tree(uint64_t) noexcept { }
    free_tree(const free_tree&) noexcept = default;
    free_tree(free_tree&&) noexcept = default;
    free_tree& operator= (const free_tree&) noexcept = default;
    free_tree& operator= (free_tree&&) noexcept = default;
};
struct rooted_tree {
    uint64_t get_num_nodes() const noexcept { return 10; }
    rooted_tree() noexcept = default;
    rooted_tree(uint64_t) noexcept { }
    rooted_tree(const rooted_tree&) noexcept = default;
    rooted_tree(rooted_tree&&) noexcept = default;
    rooted_tree& operator= (const rooted_tree&) noexcept = default;
    rooted_tree& operator= (rooted_tree&&) noexcept = default;
};

enum class bounding_t { bound, unbound };
template<bounding_t b>
static constexpr bool is_collection_bound = b == bounding_t::bound;
template<bounding_t b>
static constexpr bool is_collection_unbound = b == bounding_t::unbound;

// -----------------------------------------------------------------------------

template <typename tree_t>
struct base_tree {
    base_tree(const tree_t& t) noexcept : m_t(t) { }
    ~base_tree() noexcept = default;
    base_tree(const base_tree& bt) noexcept = default;
    base_tree(base_tree&& bt) noexcept = default;
    base_tree& operator= (const base_tree& bt) noexcept = default;
    base_tree& operator= (base_tree&& bt) noexcept = default;

    const tree_t& m_t;
};

template <typename tree_t>
struct base_empty {
    base_empty() noexcept = default;
    ~base_empty() noexcept = default;
    base_empty(const base_empty& bt) noexcept = default;
    base_empty(base_empty&& bt) noexcept = default;
    base_empty& operator= (const base_empty& bt) noexcept = default;
    base_empty& operator= (base_empty&& bt) noexcept = default;
};

template <typename tree_t, bounding_t bound>
using base_impl =
    std::conditional_t<is_collection_bound<bound>,
                       base_tree<tree_t>,
                       base_empty<tree_t>>;

template <typename tree_t, bounding_t bound>
class arrangement_collection : public base_impl<tree_t, bound> {
public:
    template <typename t, bounding_t b> using ac = arrangement_collection<t, b>;

public:
    // ------------------
    // BASIC CONSTRUCTORS

    // when bound == bounding_t::bound
    template <bounding_t cb = bound, std::enable_if_t<is_collection_bound<cb>, bool> = true>
    arrangement_collection(const tree_t& t) noexcept
        : base_tree<tree_t>(t), m_num_nodes(t.get_num_nodes())
    { }

    // when bound == bounding_t::unbound
    template <bounding_t cb = bound, std::enable_if_t<is_collection_unbound<cb>, bool> = true>
    arrangement_collection(uint64_t n) noexcept
        : base_empty<tree_t>(), m_num_nodes(n)
    { }

    // -----------------
    // COPY CONSTRUCTORS

    // for bound == bounding_t::bound
    template <
        typename otree_t,
        bounding_t cb = bound, std::enable_if_t<is_collection_bound<cb>, bool> = true
    >
    arrangement_collection(const arrangement_collection<otree_t, bounding_t::bound>& ac) noexcept
        : base_tree<tree_t>(ac.m_t), m_num_nodes(ac.m_num_nodes)
    { }

    template <
        typename otree_t,
        bounding_t cb = bound, std::enable_if_t<is_collection_bound<cb>, bool> = true
    >
    arrangement_collection(const tree_t& t, const arrangement_collection<otree_t, bounding_t::unbound>& ac) noexcept
        : base_tree<tree_t>(t), m_num_nodes(ac.m_num_nodes)
    { }

    // for bound == bounding_t::unbound
    template <
        typename otree_t, bounding_t b,
        bounding_t cb = bound, std::enable_if_t<is_collection_unbound<cb>, bool> = true
    >
    arrangement_collection(const arrangement_collection<otree_t, b>& ac) noexcept
        : base_empty<tree_t>(), m_num_nodes(ac.m_num_nodes)
    { }

    // -------------------------
    // COPY ASSIGNMENT OPERATORS

    arrangement_collection& operator= (const arrangement_collection& ac) noexcept
    {
        m_num_nodes = ac.m_num_nodes;
        return *this;
    }

    void set_num_nodes(uint64_t n) noexcept { m_num_nodes = n; }

public:
    uint64_t m_num_nodes;
};

namespace static_assertions {

typedef arrangement_collection<free_tree, bounding_t::bound> ac_free_linear_bound;
typedef arrangement_collection<free_tree, bounding_t::unbound> ac_free_linear_unbound;

typedef arrangement_collection<rooted_tree, bounding_t::bound> ac_rooted_linear_bound;
typedef arrangement_collection<rooted_tree, bounding_t::unbound> ac_rooted_linear_unbound;

static_assert(
        std::is_constructible_v<ac_free_linear_bound, const free_tree&>
and not std::is_constructible_v<ac_rooted_linear_bound, const free_tree&>
and     std::is_constructible_v<ac_rooted_linear_bound, const rooted_tree&>
//
and     std::is_constructible_v<ac_free_linear_bound, free_tree>
and not std::is_constructible_v<ac_rooted_linear_bound, free_tree>
and     std::is_constructible_v<ac_rooted_linear_bound, rooted_tree>
//
and     std::is_constructible_v<ac_free_linear_bound, const free_tree>
and not std::is_constructible_v<ac_rooted_linear_bound, const free_tree>
and     std::is_constructible_v<ac_rooted_linear_bound, const rooted_tree>
//
and     std::is_constructible_v<ac_free_linear_bound, free_tree&>
and not std::is_constructible_v<ac_rooted_linear_bound, free_tree&>
and     std::is_constructible_v<ac_rooted_linear_bound, rooted_tree&>
//
// These classes *can* be constructed from uint64_t since free_tree is
// constructible from uint64_t
and     std::is_constructible_v<ac_free_linear_bound, uint64_t&>
and     std::is_constructible_v<ac_rooted_linear_bound, uint64_t&>
and     std::is_constructible_v<ac_free_linear_bound, uint64_t>
and     std::is_constructible_v<ac_rooted_linear_bound, uint64_t>
);

static_assert(
    not std::is_constructible_v<ac_free_linear_unbound, const free_tree&>
and not std::is_constructible_v<ac_free_linear_unbound, const rooted_tree&>
and not std::is_constructible_v<ac_rooted_linear_unbound, const free_tree&>
and not std::is_constructible_v<ac_rooted_linear_unbound, const rooted_tree&>
and     std::is_constructible_v<ac_free_linear_unbound, uint64_t>
and     std::is_constructible_v<ac_rooted_linear_unbound, uint64_t>
);

static_assert(
    std::is_copy_constructible_v<ac_free_linear_bound>
and std::is_copy_constructible_v<ac_free_linear_unbound>
and std::is_copy_constructible_v<ac_rooted_linear_bound>
and std::is_copy_constructible_v<ac_rooted_linear_unbound>
);
static_assert(std::is_copy_assignable_v<ac_free_linear_bound>);
static_assert(std::is_copy_assignable_v<ac_rooted_linear_bound>);

static_assert(
    std::is_copy_assignable_v<ac_free_linear_unbound>
and std::is_copy_assignable_v<ac_rooted_linear_unbound>
);

static_assert(
    std::is_move_constructible_v<ac_free_linear_bound>
and std::is_move_constructible_v<ac_free_linear_unbound>
and std::is_move_constructible_v<ac_rooted_linear_bound>
and std::is_move_constructible_v<ac_rooted_linear_unbound>
);

static_assert(
    std::is_move_constructible_v<ac_free_linear_bound>
and std::is_move_constructible_v<ac_free_linear_unbound>
and std::is_move_constructible_v<ac_rooted_linear_bound>
and std::is_move_constructible_v<ac_rooted_linear_unbound>
);

static_assert(
    std::is_move_assignable_v<ac_free_linear_unbound>
and std::is_move_assignable_v<ac_rooted_linear_unbound>
);
static_assert(std::is_move_assignable_v<ac_free_linear_bound>);
static_assert(std::is_move_assignable_v<ac_rooted_linear_bound>);

} // -- namespace static_assertions

std::pair<
    arrangement_collection<free_tree,bounding_t::bound>,
    arrangement_collection<free_tree,bounding_t::unbound>
>
make_arrangement_collection() noexcept
{
    free_tree ft;
    arrangement_collection<free_tree,bounding_t::bound> acb(ft);
    acb.set_num_nodes(10);

    arrangement_collection<free_tree,bounding_t::unbound> acu(10);
    acu.set_num_nodes(10);
    return std::make_pair(std::move(acb), std::move(acu));
}

arrangement_collection<free_tree,bounding_t::bound>
proxy_copy() noexcept
{
    const auto ac = make_arrangement_collection();
    const auto ac2 = ac.first;
    return ac2;
}

arrangement_collection<free_tree,bounding_t::bound>
proxy_move() noexcept
{
    const auto ac = make_arrangement_collection();
    return std::move(ac.first);
}

int main() {
    const auto accopy = proxy_copy();
    const auto acmove = proxy_move();
}

A comment on 'Minimal'

The original class template has three template parameters and twice as many lines of code.

The whole code provided is long because it contains the definition of a main() procedure, the two classes that and the assertions, but the actual class template is small.

CodePudding user response:

Your class currently doesn't have a copy constructor, since the copy constructor cannot be a template (and the ones you defined will never be used). Generating an implicitly-defined copy constructor is deprecated for types that have a user-defined copy assignment operator.

However, the default definition of the copy constructor does what you want (copying a base_empty<tree_t> has no overhead), so just arrangement_collection(const arrangement_collection&) = default; will work.

You can also rewrite this so the constructors are in base_tree<tree_t> and base_empty<tree_t>, so in arrangement_collection you can just inherit the constructors with using base_impl<tree_t, bound>::base_impl, avoiding all the enable_if SFINAE

  • Related