std::vector的std::is_copy_constructable

5

我最近阅读了这篇博客文章,讲述了为什么vector必须无条件可复制以支持不完整类型。我理解从逻辑角度来看这是必要的,因为以下内容存在复制的循环依赖:

struct Test {
    std::vector<Test> v;
};

现在我考虑是否至少可以尝试提供最佳信息。换言之,std::vector<T> 仅当 T 可以被复制或未完成时才是可复制的。因此,std::vector<std::unique_ptr<T>> 永远不会是可复制的,因为 std::unique_ptr 是只能移动的,与 T 无关。

我得出了以下解决方案:

#include <type_traits>
#include <memory>


template<class T, class = decltype(sizeof(int))>
struct is_complete : std::false_type {};

template<class T>
struct is_complete<T, decltype(sizeof(T))> : std::true_type{};

template<class T>
constexpr bool is_complete_v = is_complete<T>::value;

// Indirection to avoid instantiation of is_copy_constructible with incomplete type
template<class T, class = std::enable_if_t<is_complete_v<T>>>
struct copyable {
    static constexpr bool value = std::is_copy_constructible_v<T>;
};

template<class T>
struct copyable<T, void> : std::true_type {};

template<class T>
struct Container {

    template<class T1 = T, class = std::enable_if_t<copyable<T1>::value>>
    Container(const Container &) {}
};

struct A;
struct B{};

static_assert(!is_complete_v<A>);
static_assert(is_complete_v<B>);
static_assert(std::is_copy_constructible_v<Container<A>>);
static_assert(std::is_copy_constructible_v<Container<B>>);
static_assert(!std::is_copy_constructible_v<std::unique_ptr<A>>);
static_assert(!std::is_copy_constructible_v<std::unique_ptr<B>>);

struct A{};

static_assert(!is_complete_v<A>);

godbolt (所有 static_assert 均编译通过)

现在我有三个问题(如果它们有点不相关,对不起):

  1. 这段代码是有效的标准 C++ 代码吗?还是它在任何地方都依赖于未定义的行为?
  2. 你对这个想法有什么看法?
  3. 最初,在拷贝构造函数中我使用了 SFINAE 条件 !is_complete_v<T1> || std::is_copy_constructible_v<T1>,但我不得不添加间接性,因为否则 clang(而不是 gcc)将无法编译,因为 std::is_copy_constructible 会实例化一个不完整的类型。 || 是否也会短路模板的实例化?

关于第一个问题,我的观点是不应该存在未定义行为。可能发生的一部分是 sizeof(T),因为不应该在不完整类型上使用它。但使用 sizeof 进行 SFINAE 检查已经有很长时间了,因为它是唯一的未求值上下文,所以我认为这没问题。

关于第二个问题,我知道这会使得一个 vector<T> 是否可拷贝构造非常脆弱,因为如果在代码的一个不相关部分添加了一个完整定义的 T 的前向声明,然后也检查它的完整性,这将改变整个项目中 T 的完整性。我不确定增加的可用信息是否值得这样。


不是直接在你的代码中,但我认为你至少需要把所有东西放在一个未命名的命名空间中,以提供内部链接,否则不同的翻译单元很容易引起ODR违规,这也意味着你不能跨翻译单元边界使用容器。 - walnut
@walnut 这里展示的测试代码最好放在一个单独的源文件中,而不是与其它部分放在头文件中。模板应该是全局的,而不是在未命名的命名空间中,同时ODR冲突也不应该成为问题。 - 1201ProgramAlarm
@1201ProgramAlarm 假设所有模板都在头文件中,并且我们有两个包含它的翻译单元,但其中一个仅包括前向声明 struct A;,而另一个则包括不可复制的 A 的完整定义。然后在两个单元中使用 std::is_copy_constructible_v<Container<A>> 会导致在两个翻译单元中实例化具有两个不同定义的相同类模板,因为例如 std::enable_if_t<copyable<T1>::value> 将根据 https://timsong-cpp.github.io/cppwp/n4659/basic.def.odr#6.2 引用不同的模板特化。 - walnut
@walnut 听起来这只是将一个错误类型换成另一个,如果你的两个具有本地命名空间的不同翻译单元具有相同的代码但不同的行为。 - 1201ProgramAlarm
1个回答

2

necessary also from a logical point of view, since the following has a circular dependency on copyability:

struct Test {
    std::vector<Test> v;
};
这并不是逻辑上必要的。函数 a 可以调用函数 b,而函数 b 又调用函数 a。但是,鉴于在声明 Test 时遇到 v 的声明时必须回答问题的前提条件下,这是必要的。在当前我们所知道的 C++ 中,这是必要的,但这是由我们自己强加的各种规则导致的。
“这段代码是否是有效的标准 C++ 代码,或者它是否依赖于任何未定义的行为?”
UB。模板特化不能在不同的实例化点具有不同的含义。具体来说,“一个类模板的静态数据成员可以在翻译单元中的多个实例化点上具有多个实例” ,其中始终包括最后一个实例temp.point/7。编译器可以在翻译单元的末尾以外的其他地方实例化 is_complete<T>::value。如果这在不同的实例化点给出不同的答案,则程序是非法的。
因此,您不能使用稍后将变得完整的类型(如 Test)来实例化 is_complete

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接