【问题标题】:Is there a way to return either a new object or reference to existing object from a function?有没有办法从函数返回新对象或对现有对象的引用?
【发布时间】:2020-03-03 11:46:25
【问题描述】:

我正在尝试编写一个函数,它既可以返回对作为第一个参数传递的现有对象的引用(如果它处于正确状态),也可以使用作为第二个参数传递的字面量创建并返回一个新对象(默认)。

如果一个函数不仅可以接受文字,还可以接受另一个现有对象作为第二个(默认)参数并返回对它的引用,那就更好了。

下面是一个简单的实现,但它做了很多不需要的工作:

  1. 如果使用左值作为第二个(默认)参数调用,它会调用一个参数的复制构造函数,该参数被选择返回。理想情况下,应返回对对象的引用。

  2. 如果使用文字作为第二个(默认)参数调用,它会调用构造函数、复制构造函数和析构函数,即使没有选择第二个(默认)参数作为返回值。如果在不调用复制构造函数或析构函数的情况下构造并返回对象作为右值引用会更好。

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

有没有一种方法可以更有效地完成此任务,同时又对调用者保持简单?如果我是正确的,这需要一个函数根据函数内部的运行时决策来更改返回类型,但我想不出一个简单的调用者解决方案。

【问题讨论】:

  • 如果要返回引用,为什么返回类型不是const std::string&
  • 当然可以吃蛋糕,也可以吃。它只需要更多的工作:将字符串或std::ref 的变体返回到字符串。当然,这有点矫枉过正,但它会完成工作,尽管是以一种相当冗长、略微复杂的方式,需要每个调用者额外的工作才能正确处理结果。
  • @Rualark:这个问题似乎措辞不当。你关心的不是返回值真的;第二个参数default_st。这就是“构造函数、复制构造函数和析构函数,即使没有选择第二个(默认)参数返回”的整个概念。您想以某种方式获取此参数并根据第一个参数选择是否从中构造std::string

标签: c++ gcc parameter-passing c++17 return-by-reference


【解决方案1】:

嗯,这里有几件事。 要直接表达您的要求,您可以使用 std::variant<std::string, std::string&> 之类的东西作为您的函数返回类型。虽然我没有检查变体是否可以存储参考。 或者来自第三方库的一些等价物。要么? 您还可以编写自己的类包装字符串和字符串引用。

(不是真正的代码)

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() -&gt; string&amp; 返回一个指向该对象的字符串字段的引用(这通常是它的实现方式) - 那么当s 超出范围时 - 引用变得无效。也就是说 - 它正在引用可能已分配给其他一些对象的已释放内存。 如果您在val 中捕获该引用 - val 将引用无效数据。 如果这一切都以access violation 或信号结尾,你会很幸运。在最坏的情况下,您的程序会继续运行,但会随机崩溃。

【讨论】:

  • 嗨!感谢您提出的解决方案!现在我明白我正在寻找一个优雅的解决方案,这对调用者来说很简单。我以前没有见过这样的语法。它是否正确?枚举 { std::string s;标准::字符串& v; };
  • 啊-我在已修复的答案中犯了一些错别字。正确的语法是union {std::string s; std::string&amp; v};在 C/C++ 部分的wiki 中解释了这个概念。此外,this article 似乎也是一个很好的例子。但要明确一点 - 我认为这不是一个好方法:)
  • 值得一提的是,可以使用 std::reference_wrapper 代替直接引用。如果要实现这样一个类,我们不要忘记定义构造函数和析构函数的必要性。这就是为什么我认为这样做是个坏主意。但如果有人想尝试这个概念 - 这是godbolt playground
【解决方案2】:

我不是 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

编辑:在 OP:s 测试后制作了一个更通用的版本。它可以使用 lambda,例如
auto empty_check = [](const std::string&amp; 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;
}

【讨论】:

  • 泰德,非常感谢您的解决方案!第一个参数应该始终是左值(如问题中所示),但这很容易解决。更大的问题是在第一个函数中 std::move() 不起作用。调用复制构造函数和析构函数,而不是返回对左值的引用。由于您已经出色地解决了第二个参数为左值的情况,我们现在可以认为第一个参数始终是左值,第二个参数始终是右值。在没有复制构造函数的情况下,什么是最高效的策略?
  • 如果我不能更好地解释,我很抱歉。请查看示例代码,它显示调用了复制构造函数而不是移动构造函数:onlinegdb.com/rkMKP1Gir 我必须将 std::string 替换为调试类 TStruct 以显示调用了哪些构造函数(参见案例“First 2”)跨度>
  • 我添加了更多调试输出,现在可以看到在“First 2”情况下调用了复制构造函数(请参阅输出中的“Cc(4)”字符串)。我们能避免吗?我在第 75-77、80-82 行添加了 cmets:godbolt.org/z/y74sDG
  • @Rualark 更新了新的函数模板:godbolt.org/z/JKK-PP 回答你的问题:不,我看不到。当可选参数之一是 xvalue 时,我们必须创建一个“主体”以将其移入。我们无法根据运行时条件更改函数的返回类型。
  • 泰德,非常感谢您的回答!除了复制构造函数问题之外,这个答案似乎很完美。附言出于某种原因,我不能使用@Ted 提及您
猜你喜欢
  • 1970-01-01
  • 2014-11-14
  • 1970-01-01
  • 2018-09-16
  • 2016-03-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多