如何使用 `std::enable_if_t` 制作 class 模板 copy/move constructible/assignable

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

上下文

我正在编写一个带有三个参数的 class 模板,因此它 copy/move 可构造,copy/move 可分配。 class 有两个模板参数:(1) 一个名为 tree_t 的类型和 (2) 一个枚举值

enum class bounding_t { bound, unbound };

如果 (2) 是 bounding_t::bound 那么 class 需要有一个成员 const tree_t&;当 (2) 是 bounding_t::unbound 时,class 不应该有它。为此,我使用了 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> { }

构造函数使用 std::enable_if_t 适当地构造父 class。

尝试解决

我尝试了几种方法来使 class copy/move 可分配和 copy/move 可构造(我不确定我是否可以在不超过主体大小限制的情况下列出所有这些),但是我没能做到 none 断言失败,因此编译器不会发出任何警告。下面的 MWE 使四个断言失败,都涉及 bounding_t::bound。使这四个断言不失败的代码中唯一的变化是没有模板参数的复制赋值运算符的定义(下面MWE中这个operator=的定义有模板参数)。

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

然而,如果没有模板参数,我会收到以下我不理解的警告:

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
      |                                 ^~~~~~~~

我正在使用 g++ (Ubuntu 11.1.0-1ubuntu1~20.04) 11.1.0 和警告标志 -Wall -W

问题

如何定义此 class 模板的 copy/move constructors/assignment 运算符,以便所有断言成功且没有警告?

最小工作示例

下面的代码包含 class 定义,末尾有一些断言(std::is_constructible_v<> 和许多其他断言),以及一些说明用例的 main() 过程。

#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();
}

评论'Minimal'

原来的class模板有三个模板参数和两倍的代码行。

提供的整个代码很长,因为它包含 main() 过程的定义、两个 class 和断言,但实际的 class 模板很小。

您的 class 目前没有复制构造函数,因为复制构造函数不能是模板(并且永远不会使用您定义的模板)。对于具有 user-defined 复制赋值运算符的类型,不推荐生成 implicitly-defined 复制构造函数。

然而,复制构造函数的默认定义可以满足您的需求(复制 base_empty<tree_t> 没有任何开销),因此只需 arrangement_collection(const arrangement_collection&) = default; 即可。

你也可以重写这个,这样构造函数在base_tree<tree_t>base_empty<tree_t>中,所以在arrangement_collection中你可以只继承using base_impl<tree_t, bound>::base_impl的构造函数,避免所有enable_if SFINAE