Продление срока службы членов временных данных и дизайн API

Предположим, у меня есть кросс-платформенный класс Path, например:

class Path {
public:
    // ...
    Path parent() const;                // e.g., /foo/bar -> /foo

    std::string const& as_utf8() const {
        return path;
    }
private:
    std::string path;
};

Функция-член parent() возвращает родительский путь пути this, поэтому она (правильно) возвращает вновь созданный объект Path, который его представляет.

Для платформы, которая представляет пути на уровне ОС в виде строк UTF-8 (например, Unix), кажется разумным, чтобы as_utf8() возвращала ссылку непосредственно на внутреннее представление path, поскольку оно уже в кодировке UTF-8. .

Если у меня есть код вроде:

std::string const &s = my_path.as_utf8();  // OK (as long as my_path exists)
// ...
Path const &parent = my_path.parent();     // OK (temporary lifetime extended)

Обе эти строки хороши, потому что:

  • Если предположить, что my_path сохраняется, то s остается действительным.
  • Время жизни временного объекта, возвращаемого parent(), продлевается на const&.

Все идет нормально. Однако, если у меня есть код вроде:

std::string const &s = my_path.parent().as_utf8(); // WRONG

тогда это неправильно, потому что временный объект, возвращаемый parent(), не имеет продленное время жизни, потому что const& не ссылается на временный объект, а на член данных этого. На этом этапе, если вы попытаетесь использовать s, вы получите либо мусор, либо дамп ядра. Если бы вместо этого был код:

    std::string as_utf8() const {                 // Note: object and NOT const&
        return path;
    }

тогда код будет правильным. Однако было бы неэффективно создавать временный объект при каждом вызове этой функции-члена. Подразумевается также, что никакие "геттерные" функции-члены не должны никогда возвращать ссылки на свои данные-члены.

Если API оставить как есть, то на вызывающую сторону будет возлагаться чрезмерное бремя необходимости смотреть на тип возвращаемого значения as_utf8(), чтобы увидеть, возвращает ли она const& или нет: если это так, то вызывающая сторона должен использовать объект, а не const&; если он возвращает объект, то вызывающая сторона может использовать const&.

Итак, есть ли способ решить эту проблему, чтобы API был эффективным в большинстве случаев, но не позволял пользователю получать оборванные ссылки из, казалось бы, безобидного кода?


Кстати, это было скомпилировано с использованием g++ 5.3. Возможно, время жизни временного должно быть увеличено, но в компиляторе есть ошибка.


person Paul J. Lucas    schedule 24.08.2016    source источник
comment
тогда это неправильно, потому что время жизни временного объекта, возвращаемого parent(), не продлевается, потому что const& ссылается не на временный объект, а на его член данных это неправильно. Я ищу вопрос, который показывает, что срок службы продлен   -  person NathanOliver    schedule 24.08.2016
comment
@NathanOliver: Что не так в том, что он сказал? Он сказал, что время жизни временного объекта не продлевается. И не будет.   -  person Nicol Bolas    schedule 24.08.2016
comment
@NathanOliver [нужна ссылка]   -  person Paul J. Lucas    schedule 24.08.2016
comment
@NicolBolas Я считаю, что это и его целевое состояние, что константная ссылка на член временного является законным.   -  person NathanOliver    schedule 24.08.2016
comment
Только что разместил новый комментарий, так как я просто пропустил крайний срок редактирования   -  person NathanOliver    schedule 24.08.2016
comment
@NathanOliver Но есть разница между temp().member и temp().get(), где get() возвращает member.   -  person aschepler    schedule 24.08.2016
comment
@aschepler Я так не думаю. Пока get возвращает ссылку на член, у вас есть член, поскольку ссылка является просто псевдонимом.   -  person NathanOliver    schedule 24.08.2016
comment
@NathanOliver: Как компилятор мог возможно узнать, что get возвращает ссылку на член? Компилятор может видеть только то, что get возвращает ссылку. Нет, временное продление срока службы происходит только тогда, когда вы получаете NSDM, а не вызываете функцию-член.   -  person Nicol Bolas    schedule 24.08.2016
comment
@NathanOliver Как компилятор узнает, что нужно продлить время жизни временного файла, если get не является встроенным?   -  person aschepler    schedule 24.08.2016
comment
Хорошо, так что только если вы напрямую ссылаетесь на член, компилятору разрешено продлить время жизни. Спасибо.   -  person NathanOliver    schedule 24.08.2016


Ответы (2)


Что вы можете сделать, так это создать 2 разные версии as_utf8(), одну для использования с lvalue и одну для rvalue. Однако вам понадобится С++ 11.

Таким образом, вы получаете лучшее из обоих миров: const&, когда объект не является временным, и эффективный ход, когда он таковым не является:

std::string const& as_utf8() const & {
                               // ^^^ Called from lvalues only
    return path;
}

std::string as_utf8() const && {
                        // ^^^^ Called from rvalues only
    return std::move(path); //We don't need path any more
}
person Rakete1111    schedule 24.08.2016
comment
Что бы вы сделали, если бы на одной из поддерживаемых вами платформ не было компилятора C++11? - person Paul J. Lucas; 24.08.2016
comment
@PaulJ.Lucas Ну, это зависит. Я бы использовал другую платформу для компиляции кода для платформы, которую мне нужно было поддерживать :) Если это невозможно, я бы использовал вторую версию. Учитывая, что path в большинстве случаев не очень длинный, потеря производительности, на мой взгляд, не имеет большого значения, поскольку она минимальна :) - person Rakete1111; 24.08.2016
comment
@PaulJ.Lucas: я бы перестал беспокоиться о тривиальных проблемах эффективности, подобных этой, и просто вернул бы std::string по значению. На самом деле, если у ОП нет доказательств того, что эти копии дороги, я бы все равно это сделал. - person Martin Bonner supports Monica; 24.08.2016
comment
Я бы переместил path для rvalue, чтобы избежать копирования. - person Jarod42; 24.08.2016
comment
@ Jarod42 Разве это не мешает компилятору использовать RVO? - person Rakete1111; 24.08.2016
comment
path не является локальной переменной, поэтому NRVO здесь не применяется. - person Jarod42; 24.08.2016
comment
@Jarod42 Интересно :) Спасибо - person Rakete1111; 24.08.2016

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

т. е. представляет ли метод простое свойство (приводит аргументы в пользу ссылки, особенно если оно неизменяемо) или он что-то генерирует?

Если он создает новый объект или представление, мы можем разумно ожидать, что он вернет отдельный объект.

Пользователи API обычно привыкли понимать, что свойства не живут дольше своих основных объектов. Это, конечно, можно сделать ясно в документации.

e.g.

struct path
{
    /// a property
    /// @note lifetime is no longer than the lifetime of this object
    std::string const& native() const;

    /// generate a new string representation in a different format
    std::string to_url() const;

};

Лично я бы избегал префикса as_ в этом случае, так как для меня это предполагает, что мы возвращаем новое представление того же объекта, например:

struct world 
: std::enable_shared_from_this<world>
{
    struct sky {} my_sky_;

    /// returns a shared_ptr to my sky object, which shares its lifetime
    /// with this world.
    std::shared_ptr<sky> as_sky() 
    { 
      return std::shared_ptr<sky>(shared_from_this(), std::addressof(my_sky_));
    }
};
person Richard Hodges    schedule 24.08.2016