初始化数组时,我可以避免c++11移动吗

can I avoid a c++11 move when initializing an array?

本文关键字:c++11 移动 可以避免 数组 初始化      更新时间:2023-10-16

我使用C++11功能来制作我自己的StrCat的较小实现,部分原因是为了尝试C++11可变模板。(同时,为了避免依赖一个新的库来获得我可以在几行代码中编写的东西,或者将较长的代码复制到我自己的程序中,添加关于其许可证的注释,等等)。

我的实现似乎可以工作,但如果没有StrCatPiece的move构造函数,我就无法做到这一点。这很麻烦,因为我看不出默认的移动构造函数是如何安全的:如果原始的StrCatPiece的piece_引用了buf_内的地址,那么新的StrCatPiece的piece_也将引用原始的buf_内的一个地址,而不是新的buf_。我看不出任何保证原始缓冲区会一直存在,直到它不再被引用。

$ g++ --version
g++ (Ubuntu 5.2.1-22ubuntu2) 5.2.1 20151010
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

无移动构造函数:

$ g++ -Wall -g --std=c++11 strcattest.cc -o strcattest
strcattest.cc: In instantiation of ‘std::__cxx11::string StrCat(Types ...) [with Types = {const char*, int}; std::__cxx11::string = std::__cxx11::basic_string<char>]’:
strcattest.cc:54:33:   required from here
strcattest.cc:39:38: error: use of deleted function ‘StrCatPiece::StrCatPiece(const StrCatPiece&)’
   auto pieces = {StrCatPiece(args)...};
...

我的编译器显然忽略了我的自定义移动构造函数

$ g++ -Wall -g --std=c++11 strcattest.cc -o strcattest
$ ./strcattest 
foo 26

但是,如果我强迫它与调用abort()的自定义移动构造函数一起运行,程序就会像预期的那样崩溃:

$ g++ -Wall -g -fno-elide-constructors --std=c++11 strcattest.cc -o strcattest
$ ./strcattest
Aborted

如果我有一个默认的move构造函数,它似乎可以工作,但我很怀疑。。。所以在gdb中,我可以确认StringPiece指向当前缓冲区之外的其他地方:

(gdb) break 39
Breakpoint 1 at 0x401249: file strcattest.cc, line 39.
(gdb) run
Starting program: /home/slamb/strcattest 
Breakpoint 1, StrCat<char const*, int> () at strcattest.cc:39
39    size_t size = 0;
(gdb) print pieces
$1 = {_M_array = 0x7fffffffe9b0, _M_len = 2}
(gdb) print pieces._M_array
$2 = (std::initializer_list<StrCatPiece>::iterator) 0x7fffffffe9b0
(gdb) print pieces._M_array[1]
$3 = {piece_ = {ptr_ = 0x7fffffffe972 "26", length_ = 2, 
    static npos = <optimized out>}, 
  buf_ = "0600000000000000360350G367377177000001006266"}
(gdb) print (void*)pieces._M_array[1].buf_
$4 = (void *) 0x7fffffffe9e8
(gdb) print (void*)pieces._M_array[1].buf_ + 20
$6 = (void *) 0x7fffffffe9fc

特别是,0x7fffffffe972不在[0x7ffffffffe9e8,0x7ff fffffe9fc)!中

我可以定义一个有效的复制构造函数,但我想知道是否有办法完全避免复制/移动。

这是代码:

// Compile with: g++ -Wall --std=c++11 strcattest.cc -o strcattest
#include <re2/stringpiece.h>
#include <stdlib.h>
#include <iostream>
class StrCatPiece {
 public:
  explicit StrCatPiece(uint64_t p);
  explicit StrCatPiece(re2::StringPiece p) : piece_(p) {}
  StrCatPiece(const StrCatPiece &) = delete;
  //StrCatPiece(StrCatPiece &&) { abort(); }
  StrCatPiece &operator=(const StrCatPiece &) = delete;
  const char *data() const { return piece_.data(); }
  size_t size() const { return piece_.size(); }
 private:
  re2::StringPiece piece_;
  char buf_[20];  // length of maximum uint64 (no terminator needed).
};
StrCatPiece::StrCatPiece(uint64_t p) {
  if (p == 0) {
    piece_ = "0";
  } else {
    size_t i = sizeof(buf_);
    while (p != 0) {
      buf_[--i] = '0' + (p % 10);
      p /= 10;
    }
    piece_.set(buf_ + i, sizeof(buf_) - i);
  }
}
template <typename... Types>
std::string StrCat(Types... args) {
  auto pieces = {StrCatPiece(args)...};
  size_t size = 0;
  for (const auto &p : pieces) {
    size += p.size();
  }
  std::string out;
  out.reserve(size);
  for (const auto &p : pieces) {
    out.append(p.data(), p.size());
  }
  return out;
}
int main(int argc, char** argv) {
  std::cout << StrCat("foo ", 26) << std::endl;
  return 0;
}

编辑:添加一个复制构造函数当然有效:

StrCatPiece::StrCatPiece(const StrCatPiece &o) {
  const char* data = o.piece_.data();
  if (o.buf_ <= data && data < o.buf_ + sizeof(o.buf_)) {
    memcpy(buf_, data, o.piece_.size());
    piece_.set(buf_, o.piece_.size());
  } else {
    piece_ = o.piece_;
  }
}

仍然很好奇是否可以完全避免移动或复制StrCatPiece(始终,而不仅仅是作为编译器优化),或者如果不能,为什么不呢。

有一种相当简单的方法可以做到这一点。聚合初始化是一个复制初始化上下文,因此初始化数组元素而不产生概念复制的唯一方法是通过复制列表初始化,即从{braced-init-list}初始化,但这不能使用显式构造函数,因此我们需要为StrCatPiece提供一个非显式的构造函数,最好不要创建不需要的隐式转换。

因此,一个简单的包装类模板来包装实际的构造函数参数:

template<class U>
struct StrCatPieceArg {
    explicit StrCatPieceArg(U u) : u(u) {}
    U u;
};

以及StrCatPiece的非explicit构造函数,它接受StrCatPieceArg,并将封装的参数转发给实际的构造函数。

template<class U>
StrCatPiece(StrCatPieceArg<U> arg) : StrCatPiece(arg.u) {}

这里并没有真正失去任何显式,因为获得StrCatPieceArg的唯一方法是使用其显式构造函数。

这个想法的另一个变体是使用一个额外的标记参数而不是包装类模板来表示"是的,我真的想构造一个StrCatPiece"。

我们现在可以制作一个StrCatPiece的数组,注意复制列表初始化每个元素,这样就不会创建临时的,即使在概念上也是如此:

template <typename... Types>
std::string StrCat(Types... args) {
    StrCatPiece pieces[] = {{StrCatPieceArg<Types>(args)}...};
                         // ^                           ^  
                         // These braces are important!!!
    //...
}