有没有办法在函数调用中通过引用传递和通过值显式传递?

Posted

技术标签:

【中文标题】有没有办法在函数调用中通过引用传递和通过值显式传递?【英文标题】:Is there a way to make passing by reference, and passing by value explicit in the function call? 【发布时间】:2013-11-17 00:38:17 【问题描述】:

如果您要查看这段代码,

int x = 0;
function(x);
std::cout << x << '\n';

您将无法通过任何语法方式验证参数 x 是通过引用传递还是通过值传递。您确定的唯一方法是查看函数声明或函数定义。

这是一个简单的例子,说明我认为这可能是一个问题:

std::string Lowercase(std::string str); //<- this is hidden away in code; probably in a different file.

int main()
    std::string str = "HELLO";
    Lowercase(str);
    std::cout << str << '\n'; //<- Bug! we expected to output "hello".  The problem is not very easy to spot, especially when a function name sounds as though it will change the passed in value.

为了避免在函数调用和函数声明(或在某些情况下,文档)之间跳转以了解函数行为,有没有办法在函数调用的语法中显式记录参数预计会改变(即引用参数)或正在发送副本(即按值传递)?

我意识到也可以选择通过 const& 传递,它与按值传递的概念相似,因为传入的变量在函数调用后不会改变其值。


我确信该语言中存在各种可能会增加理解参数传递方式的复杂性的情况 - 但我很好奇,有没有办法以我想要的方式解决这个问题?

我注意到有些人编写了两个相似的函数。其中一个采用值参数,另一个采用指针。这允许调用这样的函数:

Lowercase(str); //we assume the value will not change
Lowercase(&str); //we assume the value will change

但是这个解决方案还有很多其他问题,我不想失去参考的好处。此外,我们仍在对行为做出假设。

【问题讨论】:

某些用户会杀了我,把我切成四份,然后用热油烧掉我的部队,但要实现这一点:使用指针。 (哦,等等,实际上并没有那么糟糕。即使是谷歌的人也这样做。) @H2CO3 我也在 Facebook 的“愚蠢”库中看到了它。 Stroustrup 对此的看法:“为了保持程序的可读性,通常最好避免使用修改其参数的函数。[...] 因此,应该使用“普通”引用参数仅当函数名称强烈暗示引用参数已被修改时” [TC++PL,第 4 版,pp.191] 如果你不知道一个函数是做什么的,你不应该调用它。如果您正在维护代码并且遇到函数调用并且您不知道它的作用,那么 RTFM。如果没有 FM,那么您的开发过程就会中断。 【参考方案1】:

有些人坚持认为传递可变对象的正确方法是使用指针。也就是说,你会通过

Lowercase(&str);

... 和Lowercase() 显然会被实现为采用指针。这种方法可能适合您的需求。

然而,我想提一下,这不是我会做的!相反,我喜欢的方法是使用适当的名称。例如,

inplace_lowercase(str);

几乎说它要做什么。很明显,inplace_lowercase() 实际上是一种算法,稍微有点魔法就可以合理地称为

inplace_lowercase(str.begin() + 1, str.end());

也是。

以下是我不喜欢通过指针传递参数和/或为什么我不相信参数是如何传递的明确指示的几个原因:

指针可以为空。在我看来,一个强制性的参考参数应该是一个参考。 通过指针传递仍然不能表明参数是否可以修改,因为参数可能是T const*。 拥有有意义的名称实际上可以更轻松地了解首先发生的事情。 在没有查阅其文档和/或知道被调用函数将做什么的情况下调用某项内容无论如何都不起作用,并指出事情是如何传递的,这是试图解决更深层次问题的症状。

【讨论】:

+1 表示对象可能不存在时接受指针;如果对象必须存在,并且您打算更改它,则接受引用;如果对象必须存在并且您不打算更改它,则接受 const 引用;如果参数是一个值而不是一个对象,则接受一个值(诚然有些模棱两可)。 [我知道这并没有解决原始问题,因此是评论,而不是答案。] 让它接受三个参数lowercase(from_start, from_end, to_start)会不会更好?这样你就可以去掉“inplace_”前缀,让用户明确地说明输出应该去哪里(同时仍然只有一个函数可以做“小写”)。 @elmo: inplace_lowercase() 可能只是转发到另一种算法,例如,lowercase() 就像你描述的那样。就地版本可能更方便使用。无论哪种情况,我的观点是界面设计有比指示参数传递方式更好的替代方案。【参考方案2】:

我不确定我是否完全理解您的要求,但也许这是您可以使用的:

template<typename T>
void foo( T )  static_assert( sizeof(T)==0, "foo() requires a std::ref" ); 

void foo( std::reference_wrapper<int> t )

    // modify i here via t.get() or other means of std::reference_wrapper


int main()

    int i = 42;
    // foo( i ); // does not compile, static_assert fires
    foo( std::ref( i ) ); // explicit std::ref visible on the caller's side

【讨论】:

或者您可以编写一个类似于reference_wrapper 的自己的类型,但使用显式/私有 ctor。 (还有一个隐含的包含static_assert。) Live example 的替代品。 @DyP 不错的替代方案,它避免了为用户提供重载的需要!【参考方案3】:

许多(大多数)IDE 通过在确定您正在调用的函数后显示函数/方法原型来帮助您解决此问题。

【讨论】:

【参考方案4】:

这是 C++:缺少 inout 参数并不意味着该语言有缺陷,这意味着您需要实现其他语言作为库的语言功能。

创建两个template 类和函数。

in_param&lt;T&gt;T const&amp; 的包装器,而 io_param&lt;T&gt;T&amp; 引用的包装器。您可以通过调用辅助函数 inio 来构建它们。

在内部,它们的行为类似于引用(通过重载)。

在外部,调用者必须在参数上调用inio,并在调用点进行标记。

out 比较棘手:在函数内部,只有赋值是合法的。理想情况下,我们甚至不会构造它:emplace 方法可能会有所帮助。

但是,调用者需要一些通道来知道参数是否被构造。

我要做的是out_param 只有operator=,它会分配。 out 将某些内容包装到 out_param 中。如果您想要延迟构造,请在 out 参数内使用 optional,它会接近。也许out_param 也有emplace,这通常只是分配,但如果tyoe 包裹有emplace 来代替调用它?

template<typename T>
struct in_param : std::reference_wrapper<T const> 
  explicit in_param( T const& t ):std::reference_wrapper<T const>(t) 
  in_param( in_param<T>&& o ):std::reference_wrapper<T const>(std::move(o)) 
  void operator=( in_param<T> const& o ) = delete;
;
template<typename T>
struct io_param : std::reference_wrapper<T> 
  explicit io_param( T& t ):std::reference_wrapper<T>(t) 
  io_param( io_param<T>&& o ):std::reference_wrapper<T>(std::move(o)) 
;
template<typename T>
in_param< T > in( T const& t )  return in_param<T>(t); 
template<typename T>
io_param< T > io( T& t )  return io_param<T>(t); 

template<typename T>
struct out_param 
private:
  T& t;
public:
  out_param( T& t_ ):t(t_) 
  out_param( out_param<T>&& o ):t(o.t) 
  void operator=( out_param<T> const& o ) = delete;
  void operator=( out_param<T> && o ) = delete;
  void operator=( out_param<T> & o ) = delete;
  void operator=( out_param<T> && o ) = delete;
  template<typename U>
  out_param<T>& operator=( U&& u ) 
    t = std::forward<U>(u);
    return *this;
  
  // to improve, test if `t` has an `emplace` method.  If it does not,
  // instead do t = T( std::forward<Us>(us)... ). (I'd use tag dispatching
  // to call one of two methods)
  template<typename... Us>
  void emplace( Us&&... us ) 
    t.emplace( std::forward<Us>(us)... );
  
;
template<typename T>
out_param<T> out( T& t )  return out_param<T>(t); 

或者类似上面的东西。

你现在得到如下语法:

void do_stuff( int x, in_param<expensive> y, io_param<something> z, out_param<double> d );

int main() 
  expensive a;
  something b;
  double d;
  do_stuff( 7, in(a), io(b), out(d) );

在调用站点调用 inioout 失败会导致编译时错误。另外,out_param 使得在函数中意外读取 out 变量的状态变得非常困难,从而在调用站点生成了一些非常好的文档。

【讨论】:

【参考方案5】:

如果您使用 MS VC++,那么它可能是有关源代码注释语言 (SAL) 的有用信息 http://msdn.microsoft.com/ru-ru/library/hh916383.aspx

【讨论】:

【参考方案6】:

我认为通知是无用的(尽管如此,通过语言 [1])。唯一需要的问题是:“我的对象是否在语义上被修改了?”等等:

阅读原型时,您就知道函数是否可以修改对象(非 const ref)(复制或 const ref)。 在使用函数时(即使没有阅读 [2] 原型),如果必须确保不修改对象,请使用 const_cast。

[1] 静态分析器可以满足其目的。 [2] 如果你错过了,编译器无论如何都会警告你。

【讨论】:

【参考方案7】:

这就是传递引用的全部意义——从语法上讲,它不需要做任何不同于按值传递的事情。

【讨论】:

以上是关于有没有办法在函数调用中通过引用传递和通过值显式传递?的主要内容,如果未能解决你的问题,请参考以下文章

在 C 中通过引用传递时会发生啥?

有没有办法在C ++中通过引用传递rvalue?

在 C++ 中通过引用传递 char 的效率

在 C++ 中通过引用/值传递

在 C# 中通过引用传递对象和对象列表

在python中通过引用传递引用