samedi 5 décembre 2015

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

I used C++11 features to make my own smaller implementation of StrCat, in part to try out C++11 variadic templates. (Also to avoid the annoyance of either depending on a new library just for something I can write in a few lines of code or copying the longer code into my own program, adding a note about its license, etc).

My implementation seems to work, but I wasn't able to do it without having a move constructor for StrCatPiece. That's troubling because I don't see how the default move constructor could be safe: if the original StrCatPiece's piece_ refers to an address within buf_, the new StrCatPiece's piece_ will also refer to an address within the original buf_, not the new buf_. I don't see any guarantee the original buffer will live until it's no longer referenced.

$ 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.

Without move constructor:

$ 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)...};
...

My custom move constructor is apparently elided by my compiler:

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

but if I force it to run with my custom move constructor that calls abort(), the program crashes as expected:

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

If I instead have a default move constructor, it seems to work, but I'm suspicious...so in gdb I can confirm that the StringPiece points somewhere other than the current buffer:

(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_ = "\006\000\000\000\000\000\000\000\360\350G\367\377\177\000\000\001\000\062\066"}
(gdb) print (void*)pieces._M_array[1].buf_
$4 = (void *) 0x7fffffffe9e8
(gdb) print (void*)pieces._M_array[1].buf_ + 20
$6 = (void *) 0x7fffffffe9fc

In particular, 0x7fffffffe972 is not in [0x7fffffffe9e8, 0x7fffffffe9fc)!

I could define a working copy constructor, but I'm wondering if there's a way to avoid the copy/move entirely.

Here's the code:

// 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;
}

Aucun commentaire:

Enregistrer un commentaire