Безопасно ли возвращать std :: string по значению?

В следующем коде строка инкапсулирована в класс Foo.

Вызов Foo :: getText () возвращает строку по значению. Это создает второй экземпляр строкового объекта, но теперь оба строковых объекта указывают на один и тот же буфер символов в куче.

Когда экземпляр Foo удаляется, инкапсулированная строка автоматически уничтожается, и поэтому буфер символов в куче удаляется.

Несмотря на то, что getText() возвращает строку по значению, результирующий строковый объект по-прежнему полагается на время жизни исходного строкового объекта, чтобы сохранить буфер символов, на который они взаимно указывают.

Разве это не означает, что печать retval на терминал является недопустимым доступом к памяти, которая уже была освобождена в куче?

class Foo
{
    Foo(const char* text) :
       str(text)
    {
    }

    std::string getText()
    {
        return str;
    }

    std::string str;
};

int main()
{
    Foo pFoo = new Foo("text");
    std::string retval = foo.getText();
    delete pFoo;
    cout << retval;  // invalid memory access to char buffer?
}

Я думаю, что многие люди предполагают, что, поскольку строка была возвращена по значению, им не нужно беспокоиться о времени жизни исходной строки в Foo. Эта проблема не связана строго со строками, но действительно применима к любому классу с инкапсулированными указателями, которые освобождаются при уничтожении. Но что здесь лучше всего, когда дело доходит до струнных?

  1. Никогда не возвращать строку по значению?
  2. Возвращать строки только по значению, если время жизни исходной строки гарантировано?
  3. Всегда делать копию строки? return std::string(retval.c_str());
  4. Обеспечить соблюдение контракта с вызывающим абонентом getText()?

РЕДАКТИРОВАТЬ:

Я думаю, что меня ввело в заблуждение РВО. Все три строки в этом примере возвращают c_str по одному и тому же адресу. Виноват РВО?

class Obj
{
public:
    Obj() : s("text")
    {
        std::printf("%p\n", s.c_str());
    }

    std::string getText() { return s; }

    std::string s;
};

int main()
{
    Obj* pObj = new Obj();
    std::string s1(pObj->getText());
    std::string s2 = pObj->getText();
    delete pObj;
    std::printf("%p\n", s1.c_str());
    std::printf("%p\n", s2.c_str());
}

Результат:

0x600022888
0x600022888
0x600022888

person Jason    schedule 23.06.2016    source источник
comment
@ πάντα ῥεῖ Речь идет о std::string, а не о std::vector. Еще одна неудачная попытка.   -  person Lightness Races in Orbit    schedule 24.06.2016
comment
Несмотря на то, что getText () возвращает строку по значению, результирующий строковый объект по-прежнему полагается на время жизни исходного строкового объекта, чтобы сохранить буфер символов, на который они взаимно указывают. - Что ?? Откуда вы это взяли, или вы догадываетесь? C ++ - это не Java, если вы используете Java в качестве справочника при выяснении того, как работает C ++.   -  person PaulMcKenzie    schedule 24.06.2016
comment
но оба строковых объекта теперь указывают на один и тот же буфер символов в куче. - Если класс закодирован как множество попыток новичка в самодельных строковых классах с ошибочным или отсутствующим конструктором копирования, оператором присваивания , и деструктор, тогда да, он может ошибочно указывать на один и тот же буфер. Однако мы говорим о std :: string, написанном профессионалами и экспертами, поэтому имеет правильную семантику копирования.   -  person PaulMcKenzie    schedule 24.06.2016
comment
@PaulMcKenzie Я экспериментировал выше, чтобы прийти к такому выводу. Зачем кому-либо писать свой собственный строковый класс?   -  person Jason    schedule 24.06.2016
comment
@Jason Вы не поверите, сколько сообщений мы получаем от тех, кто занимается домашним заданием, которые хотят написать свой собственный строковый класс. И да, если вы посмотрите на их попытки, вы бы пришли к выводу, что буфер является общим.   -  person PaulMcKenzie    schedule 24.06.2016
comment
@PaulMcKenzie Понятно. Можете ли вы объяснить, почему каждый вызов c_str () возвращает указатель на один и тот же массив?   -  person Jason    schedule 24.06.2016
comment
Я не удивлюсь, если тот же адрес исходит от компилятора, оптимизирующего и просто загружающего адрес строки из метки для текста.   -  person chris    schedule 24.06.2016
comment
@chris Я установил флаг -O0, но все равно получил тот же ответ. Возможно, это неизбежно.   -  person Jason    schedule 24.06.2016
comment
@Jason. Если теперь вы измените содержимое s1, чтобы оно отличалось от s2, вы, скорее всего, заметили бы разницу в указателях.   -  person PaulMcKenzie    schedule 24.06.2016
comment
Здесь нужно немного углубиться в пространство языкового юриста, но идет ли спецификация настолько далеко, что запрещает использование распределителя, который достаточно умен, чтобы ссылаться на подсчет дублированных строк, пока он скрывает этот факт от пользователя?   -  person user4581301    schedule 24.06.2016
comment
@PaulMcKenzie Конечно, но меня беспокоит право собственности на исходный буфер, помимо изменчивости.   -  person Jason    schedule 24.06.2016
comment
Совершенно уверен, что вы правы, что они используют общий буфер, по крайней мере, для GCC версии 4.8–4.9, но я знаю, изменится ли они после того, как вы внесете изменение. Стандартный аллокатор довольно шустрый. Если я правильно помню, до C ++ 11 не было гарантии, что c_str вернет что-либо, даже напоминающее внутреннее представление.   -  person user4581301    schedule 24.06.2016
comment
@ Джейсон Не о чем беспокоиться. См. это. Строка постоянная, буфер никуда не денется.   -  person PaulMcKenzie    schedule 24.06.2016


Ответы (2)


Это создает второй экземпляр строкового объекта, но теперь оба строковых объекта указывают на один и тот же буфер символов в куче.

Нет, они этого не делают.

std::strings владеют своим содержимым. Когда вы копируете std::string, вы копируете его буфер.

Я думаю, что многие люди предполагают, что, поскольку строка была возвращена по значению, им не нужно беспокоиться о времени жизни исходной строки в Foo.

И эти люди правы. Нет никакого «обмена».

Ваш возврат по стоимости в порядке, и вам не нужно больше об этом думать.

person Lightness Races in Orbit    schedule 23.06.2016

Я хотел бы добавить несколько моментов к ответу @ LightnessRacesInOrbit:

Никогда не возвращать строку по значению?

Никогда не возвращайте локальные string по ссылке или указателю. На самом деле, никогда не возвращайте ничего локально по ссылке или указателю. По стоимости это нормально.

Возвращать строки только по значению, если время жизни исходной строки гарантировано?

Опять же, вы думаете в обратном направлении. Возвращайте по ссылке или указателю, только если гарантировано время жизни оригинала.

Всегда делать копию строки? вернуть std :: string (retval.c_str ());

C ++ делает это автоматически, если не может переместить строку (RVO / NRVO). Копировать вручную не нужно.

Обеспечить соблюдение контракта с вызывающей стороной getText ()?

Не требуется, так как вы все равно получаете копию

Я думаю, что многие люди предполагают, что, поскольку строка была возвращена по значению, им не нужно беспокоиться о времени жизни исходной строки в Foo. Эта проблема не связана строго со строками, но действительно применима к любому классу с инкапсулированными указателями, которые освобождаются при уничтожении.

И их предположение верно. Любой класс с (рабочим) копирующим конструктором можно копировать сколь угодно часто, и каждый копируемый экземпляр полностью независим от других. Конструктор копирования заботится о копировании пространства кучи.

person tkausl    schedule 23.06.2016
comment
Спасибо, что приложили некоторые усилия к своему ответу. - person Jason; 24.06.2016