乐于分享
好东西不私藏

C++编程实践—变参模板和原地构造

C++编程实践—变参模板和原地构造

一、原位构造和变参模板

原位构造(In-Place Construction),在前面分析placement new时就详细的分析过。它是指在已经分配的的内存上直接构造对象。这样的好处非常明显,一个是可以减少内存分配和回收的开销,一个是避免一些不必要的内存拷贝提高内存的利用效率。
变参模板(Variadic Template)可以说是一种原位构造的典型的应用实践场景,现在的STL库中,有很多的实现都依赖变参模板,如果在这个过程中使用原位构造,则可以大大的提高内存利用的效率并提高写入的速度。

二、二者的关系

如果单纯的从技术角度来看,二者的实现并没有什么强的关联关系。但如果把原位构造与变参模板协同使用,则提供了一种“泛型”实现原位构造的机制。从而为容器的插入提供了更普遍的安全、高效的方式。
这也再次验证了,技术的综合应用,技巧性的组合,往往能够产生更好更优秀的实践结果。这也是为什么推荐大家需要不断的学习别人的开发经验和不同语言的经验的重要原因。

三、原理分析

在传统的容器的插入动作时,会产生大量的临时对象,产生的拷贝和移动动作也产生了不必要的内存开销。虽然在后面的C++11后提供了移动语义,但这种开销仍然不能完全避免。
但随着技术的发展,C++提供了通过变参模板和完美转发一起实现了原位构造,这样就几乎可以避免上面的各种内存处理问题。典型的就是容器中经常使用emplace和emplace_back等相关接口。它一般的实现原理如下:

  1. 处理变参模板参数
    这一步主要是参数的推导、模板参数实例化、引用折叠和参数包展开。把真正的实参准备出来
  2. 完美转发
    利用std::forward保证参数的约束的正确性,这为下一步原位构造提供安全保证
  3. placement new
    利用placement new真正的提供了原位构造的接口

也就是说,变参模板的处理,其实底层原理也是一系列的C++技术的支持,不是简单的表面上看到的那种情况。换句话说,变参模板为原位构造提供了一种更好的实现机制。

四、例程

下面看一个简单的例程:

#include<cstddef>
#include<iostream>
#include<new>
#include<string>
#include<utility>

template <typename T> classMyVector {
public:
  MyVector() = default;

  ~MyVector() {
    clear();
    ::operatordelete(data_);
  }

  MyVector(const MyVector &) = delete;
  MyVector &operator=(const MyVector &) = delete;

template <typename... Args> T &emplace_back(Args &&...args){
if (size_ == capacity_) {
      reserve(capacity_ == 0 ? 2 : capacity_ * 2);
    }

// 在已经分配内存上直接构造
    T *place = data_ + size_;
new (place) T(std::forward<Args>(args)...);

    ++size_;
return *place;
  }

voidpush_back(const T &value){ emplace_back(value); }

voidpush_back(T &&value){ emplace_back(std::move(value)); }

voidreserve(std::size_t new_capacity){
if (new_capacity <= capacity_) {
return;
    }

    T *new_data = static_cast<T *>(::operatornew(sizeof(T) * new_capacity));

std::size_t i = 0;

try {
for (; i < size_; ++i) {
new (new_data + i) T(std::move(data_[i]));
      }
    } catch (...) {
for (std::size_t j = 0; j < i; ++j) {
        new_data[j].~T();
      }

      ::operatordelete(new_data);
throw;
    }

for (std::size_t j = 0; j < size_; ++j) {
      data_[j].~T();
    }

    ::operatordelete(data_);

    data_ = new_data;
    capacity_ = new_capacity;
  }

voidclear(){
for (std::size_t i = 0; i < size_; ++i) {
      data_[i].~T();
    }

    size_ = 0;
  }

  T &operator[](std::size_t index) { return data_[index]; }

const T &operator[](std::size_t index) const { return data_[index]; }

std::size_t size() const { return size_; }

std::size_t capacity() const { return capacity_; }

private:
  T *data_ = nullptr;
std::size_t size_ = 0;
std::size_t capacity_ = 0;
};

classDemoObj {
public:
  DemoObj(std::string name, int age) : name_(std::move(name)), age_(age) { std::cout << "DemoObj constructed: " << name_ << ", " << age_ << std::endl; }

  DemoObj(const DemoObj &other) : name_(other.name_), age_(other.age_) { std::cout << "DemoObj copied\n"; }

  DemoObj(DemoObj &&other) noexcept : name_(std::move(other.name_)), age_(other.age_) { std::cout << "DemoObj moved" << std::endl; }

  ~DemoObj() { std::cout << "DemoObj destructed: " << name_ << std::endl; }

voiddisplay()conststd::cout << name_ << ", " << age_ << std::endl; }

private:
std::string name_;
int age_;
};

intmain(){
  MyVector<DemoObj> demo;

  demo.emplace_back("Demo1"18);
  demo.emplace_back("Demo2"22);
  demo.emplace_back("Demo3"31);

std::cout << "size = " << demo.size() << ", capacity = " << demo.capacity() << std::endl;

for (std::size_t i = 0; i < demo.size(); ++i) {
    demo[i].display();
  }

return0;
}

代码并不复杂,如果有过阅读前面的palcement new相关的资料的话就非常容易理解了。

五、总结

通过上面的分析可以看出,关于利用现有技术或引入更新技术进行工程创新,是技术进步的最简单也最容易实现的方式。而且,这种创新往往带来巨大的进步,这在实践中已经被反复验证过了。推荐开发者可以从此入手,对现有的工程进行小步快跑的迭代。一定会有不错的收获。