接受左值和右值参数的函数

52

有没有一种在C++中编写既能接受lvalue参数又能接受rvalue参数的函数的方法,而不将其作为模板?

例如,假设我编写了一个函数print_stream,它从istream中读取并将读取的数据打印到屏幕上或其他地方。

我认为可以像这样调用print_stream

fstream file{"filename"};
print_stream(file);

就像这样:

print_stream(fstream{"filename"});

但是我该如何声明print_stream才能使两种用法都起作用呢?

如果我将其声明为

void print_stream(istream& is);

那么第二个用法将无法编译,因为rvalue无法绑定到非const的左值引用。

如果我将其声明为

void print_stream(istream&& is);

如果我将其声明为

那么第一次使用将无法编译,因为左值无法绑定到右值引用。

void print_stream(const istream& is);

如果函数的实现尝试从 const istream 读取内容,则无法编译。

我不能将该函数设为模板并使用“通用引用”,因为其实现需要单独编译。

我可以提供两个重载版本:

void print_stream(istream& is);
void print_stream(istream&& is);

我希望第二个函数调用第一个函数,但这似乎有很多不必要的模板代码,如果每次编写具有这种语义的函数都需要这样做,那将是非常不幸的。

我能做些什么更好的方法吗?


你可以使用void print_stream(Wrapper)并将问题移至Wrapper的构造函数,这只有在您想要对多个函数执行相同操作时才有意义。 - Marc Glisse
4
“every time I write a function with semantics like this”翻译成中文是“每次我编写语义类似的函数”。“你经常编写需要使用非const、不可复制参数的函数,而且必须能接受临时对象和左值对象作为参数吗?”这是一个非常具体的情况,不必为“每次我编写语义类似的函数”而担心。如果参数是可复制的,您可以通过按值传递来解决。如果您不需要调用非const函数,则可以通过const引用方式传递参数。 - jalf
4
你可以编写一个辅助转换器 template <typename T> T & stay(T && t) { return t; },然后使用 stay(fstream { "filename" }) - Kerrek SB
你可以使用专门的模板实例来采用“通用引用”的方式,但这只是隐藏了重载的问题。 - Pixelchemist
@KerrekSB:虽然这并没有解决这个特定的问题,因为它改变了用户调用函数的方式,但我认为这是一个非常棒的技巧,在许多其他情况下会派上用场。谢谢! - HighCommander4
将其制作为模板,分别编译,显式实例化。最小化样板代码。 - n. m.
7个回答

28

除了提供两个函数重载或将您的函数变成模板,我认为没有太多明智的选择。

如果你真的非常需要一个(丑陋的)替代方案,那么我想你唯一可以做的(疯狂的)事情就是让你的函数接受一个const&,并先决条件是不能将一个const修饰的对象传递给它(反正您也不希望支持这种类型)。然后函数就可以去掉引用的const属性。

但是我个人会编写两个重载函数,并根据其中一个定义另一个函数,因此您会重复声明但不会重复定义:

void foo(X& x) 
{ 
    // Here goes the stuff... 
}

void foo(X&& x) { foo(x); }

4
这不好扩展(例如,3个参数需要8个函数)。有没有什么解决办法?我面临和原帖作者类似的问题。 - Dave
2
@Dave,像这样的东西是否适用于您的用例?http://coliru.stacked-crooked.com/a/e93ea8c3b693b593 - Mysticial
@Mysticial 这是一个相当不错的解决方法,但不幸的是,三年后我已经完全忘记了我需要它做什么。如果将来再次出现这种情况,我会记住它的! - Dave
如果我使用 foo(str1, str2),它会进入无限循环,如何添加限制以便此行会产生编译错误? - keineahnung2345

10
// Because of universal reference
// template function with && can catch rvalue and lvalue 
// We can use std::is_same to restrict T must be istream
// it's an alternative choice, and i think is's better than two overload functions
template <typename T>
typename std::enable_if<
  std::is_same<typename std::decay<T>::type, istream>::value
>::type
print(T&& t) {
  // you can get the real value type by forward
  // std::forward<T>(t)
}

6

另一个相对不太优美的替代方法是将函数设为模板,并显式实例化两个版本:

template<typename T>
void print(T&&) { /* ... */ }

template void print<istream&>(istream&);
template void print<istream&&>(istream&&);

这可以分别编译。客户端代码只需要模板的声明。

个人建议按照Andy Prowl的建议进行操作。


1
@HighCommander4 可以运行。 - jrok
它并不完全丑陋:它允许将接口与实现分离 - 这通常是一件好事。 - Kuba hasn't forgotten Monica

6
这里有一个可扩展到任意数量参数的解决方案,不需要接受函数作为模板。
#include <utility>

template <typename Ref>
struct lvalue_or_rvalue {

    Ref &&ref;

    template <typename Arg>
    constexpr lvalue_or_rvalue(Arg &&arg) noexcept
        :   ref(std::move(arg))
    { }

    constexpr operator Ref& () const & noexcept { return ref; }
    constexpr operator Ref&& () const && noexcept { return std::move(ref); }
    constexpr Ref& operator*() const noexcept { return ref; }
    constexpr Ref* operator->() const noexcept { return &ref; }

};

#include <fstream>
#include <iostream>

using namespace std;

void print_stream(lvalue_or_rvalue<istream> is) {
    cout << is->rdbuf();
}

int main() {
    ifstream file("filename");
    print_stream(file); // call with lvalue
    print_stream(ifstream("filename")); // call with rvalue
    return 0;
}

我更喜欢这种解决方案而不是其他的,因为它很符合惯用语,不需要每次都编写函数模板来使用,并且可以产生合理的编译器错误,例如...

    print_stream("filename"); // oops! forgot to construct an ifstream

test.cpp: In instantiation of 'constexpr lvalue_or_rvalue<Ref>::lvalue_or_rvalue(Arg&&) [with Arg = const char (&)[9]; Ref = std::basic_istream<char>]':
test.cpp:33:25:   required from here
test.cpp:10:23: error: invalid initialization of reference of type 'std::basic_istream<char>&&' from expression of type 'std::remove_reference<const char (&)[9]>::type' {aka 'const char [9]'}
   10 |   : ref(std::move(arg))
      |                       ^

这个解决方案的额外好处是它还支持隐式应用用户定义的转换构造函数和转换运算符...
#include <cmath>

struct IntWrapper {
    int value;
    constexpr IntWrapper(int value) noexcept : value(value) { }
};

struct DoubleWrapper {
    double value;
    constexpr DoubleWrapper(double value) noexcept : value(value) { }
};

struct LongWrapper {
    long value;
    constexpr LongWrapper(long value) noexcept : value(value) { }
    constexpr LongWrapper(const IntWrapper &iw) noexcept : value(iw.value) { }
    constexpr operator DoubleWrapper () const noexcept { return value; }
};

static void square(lvalue_or_rvalue<IntWrapper> iw) {
    iw->value *= iw->value;
}

static void cube(lvalue_or_rvalue<LongWrapper> lw) {
    lw->value *= lw->value * lw->value;
}

static void square_root(lvalue_or_rvalue<DoubleWrapper> dw) {
    dw->value = std::sqrt(dw->value);
}

void examples() {
    // implicit conversion from int to IntWrapper&& via constructor
    square(42);

    // implicit conversion from IntWrapper& to LongWrapper&& via constructor
    IntWrapper iw(42);
    cube(iw);

    // implicit conversion from IntWrapper&& to LongWrapper&& via constructor
    cube(IntWrapper(42));

    // implicit conversion from LongWrapper& to DoubleWrapper&& via operator
    LongWrapper lw(42);
    square_root(lw);

    // implicit conversion from LongWrapper&& to DoubleWrapper&& via operator
    square_root(LongWrapper(42));
}

这个真是太棒了。非常感谢!一个问题:为什么需要最后两个函数? - Ricardo
1
@Onelio: 你是在询问 operator*operator-> 吗?它们只是访问器,这样你就不必使用显式转换来从接受函数内部访问传递的引用。 - Matt Whitlock

5

勇于尝试,使用通用的前向函数并取好名字。

template<typename Stream>
auto stream_meh_to(Stream&& s) 
->decltype(std::forward<Stream>(s) << std::string{/*   */}){
    return std::forward<Stream>(s) << std::string{"meh\n"};}

请注意,这将适用于任何可以正常工作的事物,不仅限于ostream。这是非常好的。
如果函数被调用时提供了一个不合理的参数,它将简单地忽略此定义。 顺便说一句,如果缩进设置为4个空格,它会更有效。 :)
这与Cube的答案相同,只是我认为在可能的情况下,更优雅的方式是检查特定的类型,让通用编程发挥其作用。

0

这个很好用:

template<class T>
class universal_reference_wrapper : public std::reference_wrapper<T>
{
    typedef std::reference_wrapper<T> base_type;
    universal_reference_wrapper(universal_reference_wrapper const &) = delete;
public:
    constexpr universal_reference_wrapper(T &v) noexcept : base_type(v) { }
    constexpr universal_reference_wrapper(T &&v) noexcept : base_type(v) { }
    constexpr T &operator *() noexcept { return this->get(); }
    constexpr T *operator->() noexcept { return std::addressof(**this); }
};

使用方法:

#include <fstream>

void foo(universal_reference_wrapper<std::fstream> f)
{
    f->close();
}

void bar()
{
    std::fstream f;
    foo(f);
    foo(std::move(f));
}

0
如果我希望函数接管函数的参数,我倾向于将参数作为值传递,然后再移动它。但是,如果参数移动起来很昂贵(例如std::array),这并不理想。
一个典型的例子是设置对象的字符串成员:
class Foo {
   private:
      std::string name;
   public:
      void set_name( std::string new_name ) { name = std::move(new_name); }
};

使用这个函数定义,我可以调用并设置名称而不复制字符串对象:
Foo foo;
foo.set_name( std::string("John Doe") );
// or
std::string tmp_name("Jane Doe");
foo.set_name( std::move(tmp_name) );

但是如果我想保留原始值的所有权,我可以创建一个副本:

std::string name_to_keep("John Doe");
foo.set_name( name_to_keep );

这个最新版本的行为与传递const引用并进行复制赋值非常相似:

class Foo {
   // ...
   public:
      void set_name( const std::string& new_name ) { name = new_name; }
};

这对构造函数特别有用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接