【问题标题】:Does the C++ standard place explicit requirements on the vector assign function or constructors?C++ 标准是否对向量赋值函数或构造函数有明确要求?
【发布时间】:2019-01-23 16:31:36
【问题描述】:

如果有歧义,我将参考 C++14 标准。

https://en.cppreference.com/w/cpp/container/vector为参考,向量的模板参数T要求比较稀疏

对元素的要求取决于对容器执行的实际操作。一般要求元素类型是完整类型,满足Erasable的要求,但很多成员函数的要求更严格。

确实,许多页面(例如 insert 的页面)都非常明确地说明了对 T 的要求。可悲的是,构造函数或分配函数并非如此。这是网站遗漏还是标准遗漏?

动机
这个问题的动机有点复杂。我有一个类 Node 代表树中的一个节点。每个节点都有一个父节点(用Node* 表示)和一个子节点列表(用std::vector<Node, MyAllocator> 表示)。最后一点是有趣的地方。为了确保Nodes 继续指向正确的父对象,我实现了一个自定义分配器,它使用指向正确Node 的指针“预加载”每个构造函数调用。代码如下

#ifndef PRElOAD_ALLOCATOR_H
#define PRElOAD_ALLOCATOR_H

#include <tuple>
#include <utility>
#include <memory>
#include <vector>

/**
 * @class PreloadAllocator
 * Provides a generic allocator that is able to 'preload' some arguments for the
 * construct function. Note that these arguments are *always* loaded at the
 * front of any constructor call so modified move, copy constructors need to
 * created to match.
 * This allocator will only work with simple containers like vector that do not
 * modify the type of the allocator being used. *It will not work* with types
 * like list and map that internally represent the data in a different format.
 * This is because the constructors for these objects behave differently and it
 * is not simple to know how to insert the extra arguments into such a call.
 */
template <typename T, typename... Args>
class PreloadAllocator {
  public:

    // Standard allocator typedefs
    using value_type      = T;
    using pointer         = T*;
    using const_pointer   = const T*;
    using reference       = T&;
    using const_reference = const T&;
    using size_type       = std::size_t;
    using difference_type = std::ptrdiff_t;

    PreloadAllocator(Args&&... args)
      : tup(std::forward_as_tuple(args...) ) {}

    template <typename U>
      PreloadAllocator(const PreloadAllocator<U, Args...>& other)
      : tup(other.tup) {}

    pointer allocate(size_type count, const_pointer hint = 0) {
      return m_defaultAlloc.allocate(count, hint);
    }

    void deallocate(pointer ptr, size_type count) {
      return m_defaultAlloc.deallocate(ptr, count);
    }

    template <typename... Ts>
      void construct(T* ptr, Ts&&... ts) {
        // First preload the constructor arguments with the allocator's
        // versions, then add on the rest, and let the default allocator do the
        // rest.
        return construct_impl<Ts...>(
            ptr, std::forward<Ts>(ts)..., std::index_sequence_for<Args...>{});
      }

    // Hold the arguments that we will preload into every constructor call
    const std::tuple<Args...> tup;
  private:
    // Compose the default STL allocator to use its version of allocate and
    // deallocate.
    std::allocator<T> m_defaultAlloc;
    // Actual function that does the constructing
    template <typename... Ts, std::size_t... Is>
      void construct_impl(T* ptr, Ts&&... ts, std::index_sequence<Is...>) {
        return m_defaultAlloc.construct(
          ptr, std::get<Is>(tup)..., std::forward<Ts>(ts)...);
      }
};

template <typename T, typename... Args>
bool operator==(
    const PreloadAllocator<T, Args...>& lhs,
    const PreloadAllocator<T, Args...>& rhs)
{
  return lhs.tup == rhs.tup;
}

template <typename T, typename... Args>
bool operator!=(
    const PreloadAllocator<T, Args...>& lhs,
    const PreloadAllocator<T, Args...>& rhs)
{
  return !(lhs==rhs);
}

#endif //> !PRElOAD_ALLOCATOR_H

class Node {
  public:
    using alloc_t = PreloadAllocator<Node, Node*>;
    using vec_t = std::vector<Node, alloc_t>;

    Node(Node* parent, int data)
      : parent(parent), data(data), children(alloc_t(this) ) {}
    Node(Node* parent, const Node& other)
      : parent(parent), data(other.data), children(other.children, alloc_t(this) ) {}
    Node(Node* parent, Node&& other)
      : parent(parent), data(std::move(other.data) ), children(std::move(other.children), alloc_t(this) ) {}

    // Copy/Move constructing is not going to give us the correct parent!
    Node(const Node&) = delete;
    Node(Node&&) = delete;

    Node* parent;
    int data;
    vec_t children;
};

int main() {
  Node root(nullptr, 0);
}

在 g++ 中编译此代码时,它会编译并运行良好。但是,当我在 clang 中编译时,出现以下错误

In file included from alloc_test.cxx:3:
/Library/Developer/CommandLineTools/usr/include/c++/v1/vector:1275:9: error: no matching member function for call to 'assign'
        assign(_Ip(__x.begin()), _Ip(__x.end()));
        ^~~~~~
alloc_test.cxx:15:55: note: in instantiation of member function 'std::__1::vector<Node, PreloadAllocator<Node, Node *> >::vector' requested here
      : parent(parent), data(std::move(other.data) ), children(std::move(other.children), alloc_t(this) ) {}
                                                      ^
/Library/Developer/CommandLineTools/usr/include/c++/v1/vector:588:10: note: candidate function not viable: no known conversion from '_Ip' (aka 'move_iterator<__wrap_iter<Node *> >') to
      'std::__1::vector<Node, PreloadAllocator<Node, Node *> >::size_type' (aka 'unsigned long') for 1st argument
    void assign(size_type __n, const_reference __u);
         ^
/Library/Developer/CommandLineTools/usr/include/c++/v1/vector:576:9: note: candidate template ignored: requirement '!__is_forward_iterator<move_iterator<__wrap_iter<Node *> > >::value' was not satisfied
      [with _InputIterator = std::__1::move_iterator<std::__1::__wrap_iter<Node *> >]
        assign(_InputIterator __first, _InputIterator __last);
        ^
/Library/Developer/CommandLineTools/usr/include/c++/v1/vector:586:9: note: candidate template ignored: requirement 'is_constructible<value_type, typename iterator_traits<move_iterator<__wrap_iter<Node *>
      > >::reference>::value' was not satisfied [with _ForwardIterator = std::__1::move_iterator<std::__1::__wrap_iter<Node *> >]
        assign(_ForwardIterator __first, _ForwardIterator __last);
        ^
/Library/Developer/CommandLineTools/usr/include/c++/v1/vector:592:10: note: candidate function not viable: requires single argument '__il', but 2 arguments were provided
    void assign(initializer_list<value_type> __il)
         ^
1 error generated.

那么,这是 g++ 和 clang 决定以不同方式解释的标准中的歧义,还是其中一个真正的错误?

尝试编写这种预加载分配器是对标准的不可原谅的滥用吗?

*编辑:* 修复了代码 sn-p 中缺失的数据成员。

【问题讨论】:

  • 这是相当多的代码,可能会拒绝很多本来可以回答您的问题的用户。重现问题真的需要自定义分配器吗?
  • 您真的需要自定义分配器吗?为什么不在构造函数中调整孩子的父母呢?可能只是一个简单的私有成员函数,它通过children 并更新指针。显着减少代码 => 更少错误。
  • @extiam 根据标准 (C++17),您的代码是有效的,因为 Node 在带有分配器的向量中是 Cpp17MoveInsertable。这与 MoveConstructible 不同,这显然是 clang 正在测试的。 IMO,您不应使用此自定义分配器,而应使用工厂或类似的东西,它们具有相同的好处(即防止父母和孩子之间的不一致),同时更易于实现。
  • 我不太确定是否诚实。 libc++ 在其assign 中检查is_constructible,但标准中似乎没有此要求。我暂时将其称为 libc++ 中的错误。

标签: c++ g++ clang


【解决方案1】:

您的代码似乎对 C++17 标准有效。最强的需求来自std::vector的这个拷贝构造函数:

children(other.children, alloc_t(this))

标准说 (Table 65) 这要求 NodeCpp17CopyInsertablestd::vector&lt;Node, PreloadAllocator&lt;Node, Node*&gt;&gt;:

T is Cpp17CopyInsertable into X 意味着除了TCpp17MoveInsertable 转换为X 之外,以下表达式是良构的:

allocator_traits<A>::construct(m, p, v)

其求值导致以下后置条件成立:v 的值不变,等效于*p

Cpp17MoveInsertable的要求类似,只是v是右值,post条件不同。

查看clang 的错误时,编译器似乎正在检查MoveConstructible 的要求:

is_constructible<
    value_type, 
    typename iterator_traits<move_iterator<__wrap_iter<Node *> >::reference>::value

...与Cpp17MoveInsertable 的不同。


从设计的角度来看,我会亲自删除该自定义分配器,而是在需要时手动更新 parent 成员,例如:

Node(Node* parent, const Node& other) 
    : parent(parent), data(other.data), children(other.children) {
    fix_parent_in_childrens();
}

void fix_parent_in_childrens() {
    for (Node &node: children) {
        node.parent = this;
    }
}

由于您似乎已经有了正确的封装,因此(对我而言)这比自定义分配器更有意义。

【讨论】:

    猜你喜欢
    • 2013-07-21
    • 1970-01-01
    • 2018-10-31
    • 1970-01-01
    • 1970-01-01
    • 2014-08-13
    • 2019-07-16
    • 2021-01-24
    • 2014-05-09
    相关资源
    最近更新 更多