具有专门化的模板 class 中可靠的条件复制和移动构造函数

Reliable conditional copy and move constructors in template class with specializations

我发现当 T1T2 没有副本 and/or 移动构造函数时,我的 compressed_tuple<T1, T2> class 无法编译错误 "attempting to reference a deleted function"。该错误是指在 compressed_tuple 的复制构造函数中使用了已删除的复制构造函数。我了解错误是什么、为什么会出现错误以及如何解决。我不明白的是如何在没有过度专业化的情况下做到这一点(详见下文)。

compressed_tuple 是我对一对使用 EBO 通过专业化来最小化其大小的对的名称,当至少一个值是空的且可推导时。即 std::unique_ptr 通常使用此机制实现,以防止空删除器影响其大小(否则普通 std::unique_ptr 将大于指针)。

TL:DR 跳到底部,我问实际问题的地方。导致它的一切都是信息上下文。

通常我会使用专业化并收工,类似于:

template<class T>
constexpr bool is_copyable_v = 
    std::is_copy_constructible_v<T> && std::is_copy_assignable_v<T>;
template<class T>
constexpr bool is_movable_v =
    std::is_move_constructible_v<T> && std::is_move_assignable_v<T>;

template<class T>
class example_base
{
public:
    T value;

protected:
    example_base() = default;
    example_base(const example_base&) = default;
    example_base(example_base&&) = default;
    ~example_base() = default;

    inline example_base& operator=(const example_base& source)
        noexcept(std::is_nothrow_copy_assignable_v<T>)
    {
        static_assert(is_copyable_v<T>, "T must be copyable.");

        if constexpr (is_copyable_v<T>) {
            value = source.value;
        }
        return *this;
    }
    inline example_base& operator=(example_base&& source)
        noexcept(std::is_nothrow_move_assignable_v<T>)
    {
        static_assert(is_movable_v<T>, "T must be movable.");

        if constexpr (is_movable_v<T>) {
            value = std::move(source.value);
        }
        return *this;
    }
};

// T is both copyable and movable.
template<
    class T,
    bool = is_copyable_v <T>,
    bool = is_movable_v<T>>
class example final : example_base<T>
{
    using base = example_base<T>;

public:
    example() = default;
    inline example(const example& source)
        noexcept(std::is_nothrow_copy_constructible_v<T>) :
        base(source) {}
    inline example(example&& source)
        noexcept(std::is_nothrow_move_constructible_v<T>) :
        base(std::move(source)) {}

    inline example& operator=(const example& source)
        noexcept(std::is_nothrow_copy_assignable_v<T>)
    {
        return static_cast<example&>(base::operator=(source));
    }
    inline example& operator=(example&& source)
        noexcept(std::is_nothrow_move_assignable_v<T>)
    {
        return static_cast<example&>(base::operator=(std::move(source)));
    }
};

// T is copyable, but not movable.
template<class T>
class example<T, true, false> final : public example_base<T>
{
    using base = example_base<T>;

public:
    example() = default;
    inline example(const example& source)
        noexcept(std::is_nothrow_copy_constructible_v<T>) :
        base(source) {}
    example(example&&) = delete;

    inline example& operator=(const example& source)
        noexcept(std::is_nothrow_copy_assignable_v<T>)
    {
        return static_cast<example&>(base::operator=(source));
    }
    example& operator=(example&&) = delete;
};

// T isn't copyable, but is movable.
template<class T>
class example<T, false, true> final : public example_base<T>
{
    using base = example_base<T>;

public:
    example() = default;
    inline example(example&& source)
        noexcept(std::is_nothrow_move_constructible_v<T>) :
        base(std::move(source)) {}
    example(const example&) = delete;

    inline example& operator=(example&& source)
        noexcept(std::is_nothrow_move_assignable_v<T>)
    {
        return static_cast<example&>(base::operator=(std::move(source)));
    }
    example& operator=(const example&) = delete;
};

// T is neither copyable nor movable.
template<class T>
class example<T, false, false> final : public example_base<T>
{
public:
    example() = default;
    example(const example&) = delete;
    example(example&&) = delete;

    example& operator=(const example&) = delete;
    example& operator=(example&&) = delete;
};

它工作正常,但如果任何其他模板参数需要进一步专门化,则会呈指数级增长。

compressed_tuple 的所有专业都相当庞大,所以我省略了大部分内容:

// T1 is empty and inheritable, but T2 isn't, so derive from T1 and store T2.
// Handles both <..., true, true> and <..., true, false>.
template<class T1, class T2, 
    bool = std::is_empty_v<T1> && !std::is_final_v<T1>,
    bool = std::is_empty_v<T2> && !std::is_final_v<T2>>
class compressed_tuple final : private T1 
{
private:
    using base = T1;

    T2 second;

public:
    compressed_tuple(const compressed_tuple& source)
        noexcept(
            std::is_nothrow_copy_constructible_v<T1> &&
            std::is_nothrow_copy_constructible_v<T2>) :
        base(source),
        second(source.second) {}
    /*...*/
};

// T2 is empty and inheritable, but T1 isn't, so derive from T2 and store T1.
template<class T1, class T2>
class compressed_tuple<T1, T2, false, true> final : private T2 
{
private:
    using base = T2;

    T1 first;

public:
    compressed_tuple(const compressed_tuple& source)
        noexcept(
            std::is_nothrow_copy_constructible_v<T1> &&
            std::is_nothrow_copy_constructible_v<T2>) :
        base(source),
        first(source.first) {}
    /*...*/
};

// Neither T1 nor T2 are empty and derivable, so store both.
template<class T1, class T2>
class compressed_tuple<T1, T2, false, false> final
{
private:
    T1 first;
    T2 second;

public:
    compressed_tuple(const compressed_tuple& source)
        noexcept(
            std::is_nothrow_copy_constructible_v<T1> &&
            std::is_nothrow_copy_constructible_v<T2>) :
        first(source.first),
        second(source.second) {}
    /*...*/
};

我想做的事情可以通过以下方式实现:

template<
    class T,
    bool = is_copyable_v<T>,
    bool = is_movable_v<T>,
    bool = std::is_empty_v<T1> && !std::is_final_v<T1>,
    bool = std::is_empty_v<T2> && !std::is_final_v<T2>>
class compressed_tuple final { /*...*/ };

// ...specialize on all valid combinations...

虽然这需要大量的专业知识。如果可能的话,我正在寻找的是替代方案。

据我了解,SFINAE 不是解决此问题的选项。 C++20 约束可以解决这个问题,但截至撰写本文时,主流编译器与 C++20 兼容还需要相当长的时间。条件复制和移动构造函数如何在没有特化或大量特化的情况下在C++17中实现?

两种常见的方法是:

  • 默认特殊成员函数,然后安排它们被删除(例如,为此目的使用特殊基 class)。
  • "Eric's trick":

    Foo& operator=(std::conditional_t<can_copy, Foo, nonesuch> const& rhs) {
        // implement
    }
    
    Foo& operator=(std::conditional_t<!can_copy, Foo, nonesuch> const&) = delete;