【问题标题】:Creating a Node for a Linked List where the Data Lacks a Default Constructor为数据缺少默认构造函数的链表创建节点
【发布时间】:2018-07-13 11:54:23
【问题描述】:

我正在尝试实现一个表示双向链表的类,我有一个函数createNode(),它返回一个新的Node(一个模板类),它的所有成员都已初始化。此函数将用于创建大小已知但没有数据传递给它的链表。对于大多数数据类型,这是可行的。但是,这对于没有默认构造函数的类起作用,因为它们不能在没有参数的情况下被初始化。这是展示这一点的最小代码:

class Test // A class without a default constructor
{
public:
    Test(int value) : value_{ value } { };
private:
    int value_;
};

template<typename T>
struct Node
{
    Node* prev;
    Node* next;
    T value;
};

template<typename T>
Node<T>* createNode()
{
    return new Node<T>{ nullptr, nullptr, T() }; // How do I change T() so
                                                 // that I can use classes
                                                 // without default constructors?
}

int main()
{
    Node<Test>* testNode = createNode<Test>();
    delete testNode;
}

基本上,我的最终目标是能够创建一个链表,该链表可以保存未初始化的节点,同时跟踪哪些节点已初始化。我记得在我的一本旧教科书中阅读了一种解决此问题的方法,该方法涉及使用分配器(用于处理对象的构造/销毁),但我根本不记得确切的技术。那我该怎么办呢?

【问题讨论】:

  • 与您的特定问题无关,但是如果已经有std::list,您为什么还要滚动自己的列表类?有什么理由这样做吗?
  • 学习如何实现?这个链接列表显然没有任何实际用途,但了解一个链接列表的工作方式是很有价值的信息。
  • 我认为你只是分配了足够的内存并使用placement new
  • 您能否详细说明(也许举个例子)?
  • 为一个对象buf = new char[sizeof(T)];分配内存,然后在这个内存T* pT = new (buf) T(...);中构造它。您需要稍后自己调用析构函数。见en.cppreference.com/w/cpp/language/new

标签: c++ linked-list initialization c++14 default-constructor


【解决方案1】:

你所问的实际上是不可能的——在没有默认构造函数的情况下默认构造一个对象。 也许考虑将T nodeValue 参数添加到createNode()?或者更改Node 本身,使其不是持有一个对象,而是持有一个指向该对象的指针。这似乎是一场内存管理的噩梦,但它可以工作。

【讨论】:

  • 嗯,是的,我知道没有默认构造函数我无法构造类,但是管理未初始化值的正确方法是什么?至于您对分配器的第二点,我不确定这意味着什么。最后,创建一个指针可以 工作,但那将是一团糟(正如你所说)。我实际上是在寻找标准库是如何做到这一点的;您是否知道std::list 等容器在以std::list&lt;Test&gt; test(9)(节点如何初始化)等方式初始化时如何处理它们的内存?
  • @ArnavBorborah std::list 构造函数采用默认计数——默认构造所有元素。从技术上讲,它是default-inserts 它们,所以它们是通过调用分配器的construct 函数来构造的。
【解决方案2】:

如果您可以访问 C++17,请使用 std::optional&lt;T&gt;,如果您不能访问,请使用 boost::optional&lt;T&gt;

template<typename T>
struct Node
{
    Node* prev;
    Node* next;
    std::optional<T> value;  // or boost::optional<T> value;
};

template<typename T>
Node<T>* createNode()
{
    return new Node<T>{ nullptr, nullptr, std::nullopt /* or boost::none */ };
}

如果您无法访问 C++17 并且不想包含 boost,您可以使用类似这样的东西来滚动您自己的 optional 模板:

struct nullopt_t {};
nullopt_t nullopt;

template <typename T>
class optional
{
public:
    template <typename... Args>
    optional(Args&&... args)
        : ptr{new ((void*)&storage) T(std::forward<Args>(args)...)}
    {}

    optional(nullopt_t)
        : ptr{nullptr}
    {}

    ~optional()
    {
        if (ptr) {
            ptr->~T();
        }
    }

    optional& operator=(T obj)
    {
        if (ptr) {
            *ptr = std::move(obj);
        } else {
            ptr = new ((void*)&storage) T(std::move(obj));
        }
        return *this;
    }

    explicit operator bool()
    {
        return ptr != nullptr;
    }

    T& value()
    {
        if (!ptr) {
            throw std::exception();
        }
        return *ptr;
    }

    // Other const-correct and rvalue-correct accessors left
    // as an exercise to the reader
private:
    std::aligned_storage_t<sizeof(T), alignof(T)> storage;
    T* ptr;
};

Live Demo

【讨论】:

    【解决方案3】:

    您可以稍后使用placement new 将对象放置在预分配的内存中。

    这只是将内存分配与对象的构造分开。因此,您可以在 Node 中声明一个成员,该成员占用内存但不构造对象,因为它需要参数。稍后您可以使用所需的参数构造对象,但不使用new 分配内存,而是使用placement new 来调用已在Node 中分配的内存的构造函数。

    所以下面是一个自制std::optional的例子。在n3527,您可以找到有关std::optional 的更多详细信息。

    #include <vector>
    #include <functional>
    #include <iostream>
    #include <algorithm>
    #include <string>
    #include <memory>
    
    using namespace std;
    
    class Test // A class without a default constructor
    {
    public:
        Test(int value) : value_{ value } { };
    //private:
        int value_;
    };
    
    template<typename T>
    struct Node
    {
        Node* prev;
        Node* next;
    
        bool empty = true;
        union {
            T t;
        } value;    // Could be replaced with typename std::aligned_storage<sizeof(T), alignof(T)>::type value;
    
        // need a constructor that inits the value union and activate a field
        // Node()
    
        ~Node() {
            if (!empty) {
                value.t.~T();
            }
        }
        template<typename... Args>
        void setValue(Args... args) {
            if (!empty) {
                value.t.~T();
            }
            new (&value.t) T(std::forward<Args...>(args...));
            empty = false;
        }
    
        T& getValue() {
            // TODO:
            if (empty) {
                //throw
            }
            return value.t;
        }
    };
    
    template<typename T>
    Node<T>* createNode()
    {
        return new Node<T>{ nullptr, nullptr }; // How do I change T() so
                                                     // that I can use classes
                                                     // without default constructors?
    }
    
    int main()
    {
        Node<Test>* testNode = createNode<Test>();
        testNode->setValue(42);
        if (!testNode->empty) {
            std::cout << testNode->getValue().value_;
        }
        delete testNode;
        return 0;
    }
    

    Live Demo

    只需很少的小改动和reinterpret_cass,您还可以使用typename std::aligned_storage&lt;sizeof(T), alignof(T)&gt;::type value; - Live Demo

    Allocators 管理内存,您将无法在您的类中包含(聚合)对象,并且必须使用指针和第二次分配,除非您使用分配器来放置整个 Node

    在 YouTube 上有来自 John Lakos 的关于分配器的有趣演示 - CppCon 2017 Local 'Arena' Memory Allocators part 12

    【讨论】:

    • 太棒了!谢谢!
    • 但是这个例子更多是为了学习如何完成,但我会使用 std::optional。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-03-10
    • 2020-07-05
    • 2011-04-27
    • 2021-05-27
    • 2020-09-29
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多