Конструктор перемещения std :: string действительно перемещается?

Итак, я получил небольшую тестовую программу:

#include <string>
#include <iostream>
#include <memory>
#include <vector>

class Test
{
public:
  Test(const std::vector<int>& a_, const std::string& b_)
    : a(std::move(a_)),
      b(std::move(b_)),
      vBufAddr(reinterpret_cast<long long>(a.data())),
      sBufAddr(reinterpret_cast<long long>(b.data()))
  {}

  Test(Test&& mv)
    : a(std::move(mv.a)),
      b(std::move(mv.b)),
      vBufAddr(reinterpret_cast<long long>(a.data())),
      sBufAddr(reinterpret_cast<long long>(b.data()))
  {}

  bool operator==(const Test& cmp)
  {
    if (vBufAddr != cmp.vBufAddr) {
      std::cout << "Vector buffers differ: " << std::endl
        << "Ours: " << std::hex << vBufAddr << std::endl
        << "Theirs: " << cmp.vBufAddr << std::endl;
      return false;
    }

    if (sBufAddr != cmp.sBufAddr) {
      std::cout << "String buffers differ: " << std::endl
        << "Ours: " << std::hex << sBufAddr << std::endl
        << "Theirs: " << cmp.sBufAddr << std::endl;
      return false;
    }
  }

private:

  std::vector<int> a;
  std::string b;
  long long vBufAddr;
  long long sBufAddr;
};

int main()
{
  Test obj1 { {0x01, 0x02, 0x03, 0x04}, {0x01, 0x02, 0x03, 0x04}};
  Test obj2(std::move(obj1));

  obj1 == obj2;


  return 0;
}

Программное обеспечение, которое я использовал для теста:

Компилятор: gcc 7.3.0

Флаги компилятора: -std = С++ 11

ОС: Linux Mint 19 (tara) с вышедшим выпуском Ubuntu 18.04 LTS (бионический)

Результаты, которые я вижу здесь, что после перемещения векторный буфер все еще имеет тот же адрес, но строковый буфер не имеет. Так что мне кажется, что он выделил новый, а не просто поменял местами указатели буфера. Что вызывает такое поведение?

Ответ 1

Вы, вероятно, видите эффект оптимизации маленькой/короткой строки. Чтобы избежать ненужных выделений для каждой крошечной строки, многие реализации std::string включают в себя небольшой массив фиксированного размера для хранения небольших строк, не требуя new (этот массив обычно повторно использует некоторые другие члены, которые не нужны, когда динамическое выделение имеет не использовался, поэтому он потребляет мало или вообще никакой дополнительной памяти для его предоставления, как для маленьких, так и для больших string), и эти строки не получают преимущества от std::move (но они маленькие, так что это нормально). Большие строки потребуют динамического выделения и передадут указатель так, как вы ожидаете.

Просто для демонстрации, этот код на g++:

void move_test(std::string&& s) {
    std::string s2 = std::move(s);
    std::cout << "; After move: " << std::hex << reinterpret_cast<uintptr_t>(s2.data()) << std::endl;
}

int main()
{
    std::string sbase;

    for (size_t len=0; len < 32; ++len) {
        std::string s1 = sbase;
        std::cout << "Length " << len << " - Before move: " << std::hex << reinterpret_cast<uintptr_t>(s1.data());
        move_test(std::move(s1));
        sbase += 'a';
    }
}

Попробуйте онлайн!

генерирует высокие (стековые) адреса, которые изменяются в структуре перемещения на длину 15 или меньше (предположительно, зависит от размера указателя архитектуры), но переключаются на низкие (куча) адреса, которые остаются неизменными после конструкции перемещения после достижения длины 16 или выше (переключатель в 16, а не в 17, потому что это NUL -terminating строк, так как C++ 11 и выше требуют этого).

Чтобы быть на 100% ясным: это деталь реализации. Ни одна часть спецификации C++ не требует такого поведения, поэтому вы не должны полностью полагаться на то, что это происходит, и когда это происходит, вы не должны полагаться на то, что это происходит для определенных длин строк.