具有专门化的模板 class 中可靠的条件复制和移动构造函数
Reliable conditional copy and move constructors in template class with specializations
我发现当 T1
或 T2
没有副本 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;
我发现当 T1
或 T2
没有副本 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;