Есть ли способ вернуть из функции новый объект или ссылку на существующий объект? - PullRequest
1 голос
/ 07 ноября 2019

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

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

Ниже приведентривиальная реализация, но она выполняет много ненужной работы:

  1. Если вызывается с lvalue в качестве второго (по умолчанию) аргумента, он вызывает конструктор копирования аргумента, который выбран для возврата,В идеале должна быть возвращена ссылка на объект.

  2. Если вызывается с литералом в качестве второго (по умолчанию) аргумента, он вызывает конструктор, конструктор копирования и деструктор, даже если второй (по умолчанию) аргументне выбран для возврата. Было бы лучше, если бы объект создавался и возвращался как ссылка на rvalue без вызова конструктора или деструктора копирования.

std::string get_or_default(const std::string& st, const std::string& default_st) {
    if (st.empty()) return default_st
    else return st;
}

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

Ответы [ 2 ]

3 голосов
/ 07 ноября 2019

Я не уверен на 100%, что понял комбинации требований, но:

#include <iostream>
#include <string>
#include <type_traits>

// if called with an rvalue (xvalue) as 2:nd arg, move or copy
std::string get_or_default(const std::string& st, std::string&& default_st) {
    std::cout << "got temporary\n";
    if(st.empty())
        return std::move(default_st);      // rval, move ctor
        // return std::forward<std::string>(default_st); // alternative
    else
        return st;                         // lval, copy ctor
}

// lvalue as 2:nd argument, return the reference as-is
const std::string& get_or_default(const std::string& st,
                                  const std::string& default_st) {
    std::cout << "got ref\n";
    if(st.empty()) return default_st;
    else           return st;
}

int main() {
    std::string lval = "lval";

    // get ref or copy ...
    decltype(auto) s1 = get_or_default("", "temporary1");
    decltype(auto) s2 = get_or_default("", std::string("temporary2"));
    decltype(auto) s3 = get_or_default("", lval);

    std::cout << std::boolalpha;
    std::cout << std::is_reference_v<decltype(s1)> << "\n";
    std::cout << std::is_reference_v<decltype(s2)> << "\n";
    std::cout << std::is_reference_v<decltype(s3)> << "\n";
}

Вывод:

got temporary
got temporary
got ref
false
false
true

Редактировать: после ОП сделана несколько более общая версия: sтестирование. Он может использовать лямбду, например
auto empty_check = [](const std::string& s) { return s.empty(); };
, чтобы проверить, является ли первый аргумент пустым.

template<typename T, typename F>
T get_or_default(const T& st, T&& default_st, F empty) {
    if(empty(st)) return std::move(default_st);
               // return std::forward<T>(default_st); // alternative
    else          return st;
}

template<typename T, typename F>
const T& get_or_default(const T& st, const T& default_st, F empty) {
    if(empty(st)) return default_st;
    else          return st;
}
2 голосов
/ 07 ноября 2019

Ну, здесь есть несколько вещей. Чтобы выразить то, что вы просите напрямую, вы можете использовать что-то вроде std::variant<std::string, std::string&> в качестве типа возвращаемой функции. Хотя я не проверял, может ли вариант хранить ссылку. Или какой-нибудь эквивалент из сторонней библиотеки. либо <>? Вы также можете написать собственную строку переноса класса и строку ref.

(не настоящий код)

struct StringOrRef {
    enum class Type {Value, Ref} type;
    union {
        std::string value;
        std::reference_wrapper<const std::string> ref;
    };
...
};

Проверьте тему: различающее объединение в C ++.

Но я думаю, что в вашем примере есть большая проблема! Пожалуйста, рассмотрите право собственности на данные. std :: string становится владельцем переданных данных. Именно поэтому он копирует данные. Таким образом, когда ваша функция возвращает - вызываемый уверен, что у него есть данные, и ему не нужно беспокоиться об этом, пока он (а) удерживает значение.

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

Итак, рассмотрим:


StringOrRef func(strging const& a, string const& b);
...

StringOrRef val;
{ // begin scope:


SomeStruct s = get_defaul();
val = func("some value", s.get_ref_to_internal_string());

}// end of val scope

val; // is in scope but may be referencing freed data. 

Проблема здесь временного объекта SomeStruct s. если ее функция-член get_ref_to_internal_string() -> string& возвращает ссылку на строковое поле этого объекта (что часто является способом реализации) - тогда, когда s выходит из области видимости, - ссылка становится недействительной. то есть - это ссылка на освобожденную память, которая может быть передана некоторым другим объектам. И если вы захватите эту ссылку в val - val будет ссылаться на недействительные данные. Вам повезет, если все закончится access violation или сигналом. В худшем случае ваша программа продолжится, но будет зависать случайно.

...