24

如果您要查看此代码,

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

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

4

7 回答 7

25

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

Lowercase(&str);

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

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

inplace_lowercase(str);

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

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

也是。

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

  • 指针可以为空。在我看来,一个强制性的参考参数应该是一个参考。
  • 通过指针传递仍然不能指示参数是否可以修改,因为参数可能是T const*.
  • 拥有有意义的名称实际上更容易理解首先发生的事情。
  • 在没有查阅其文档和/或知道被调用函数将做什么的情况下调用某些东西无论如何都不起作用,并指出事情是如何传递的,这是试图治愈更深层次问题的症状。
于 2013-11-05T23:19:07.733 回答
9

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

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
}
于 2013-11-05T23:29:54.457 回答
3

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

于 2013-11-05T23:25:38.813 回答
2

这就是 C++:缺少inout参数并不意味着该语言有缺陷,这意味着您需要实现其他语言作为库的语言特性所做的事情。

创建两个template类和函数。

in_param<T>是 a 的包装器T const&,而 whilieio_param<T>T&引用的包装器。您可以通过调用辅助函数inio.

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

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

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

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

我要做的out_param只是 has operator=,并且它分配。 out将某些东西包装成out_param. 如果您想要延迟构造,请optional在接近的 out 参数内部使用。也许out_param还有 has emplace,通常只是分配,但如果 tyoe 包裹的 hasemplace调用它呢?

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变量的状态变得非常困难,从而在调用站点生成了一些非常好的文档。

于 2013-11-06T19:31:46.043 回答
0

我认为通知是无用的(但通过语言 [1])。唯一需要的问题是:“我的对象是否在语义上进行了修改?”,所以:

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

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

于 2013-11-05T23:37:49.490 回答
0

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

于 2013-11-05T23:23:34.947 回答
0

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

于 2013-11-07T00:02:10.620 回答