41

有没有办法在 C++ 中编写一个同时接受左值和右值参数的函数,而无需将其设为模板?

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

我认为这样称呼是合理的print_stream

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

以及像这样:

print_stream(fstream{"filename"});

但是我如何声明print_stream这样两种用法都有效?

如果我将其声明为

void print_stream(istream& is);

那么第二次使用将不会编译,因为右值不会绑定到非常量左值引用。

如果我将其声明为

void print_stream(istream&& is);

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

如果我将其声明为

void print_stream(const istream& is);

那么函数的实现将无法编译,因为您无法从const istream.

我不能使函数成为模板并使用“通用引用”,因为它的实现需要单独编译。

我可以提供两个重载:

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

并让第二个调用第一个,但这似乎是很多不必要的样板文件,我会发现每次我编写具有这样语义的函数时都必须这样做是非常不幸的。

有什么更好的我可以做的吗?

4

6 回答 6

27

我会说,除了提供两个重载或使您的函数成为模板之外,没有太多明智的选择。

如果你真的,真的需要一个(丑陋的)替代方案,那么我想你能做的唯一(疯狂的)事情就是让你的函数接受 a const&,前提是你不能将一个const-qualified 类型的对象传递给它(无论如何你都不想支持它)。然后,该函数将被允许抛弃const引用的本质。

但是我会亲自编写两个重载并根据另一个定义一个,因此您确实复制了声明,但没有复制定义:

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

void foo(X&& x) { foo(x); }
于 2013-07-14T21:55:21.913 回答
6

Another rather ugly alternative is to make the function a template and explicitly instantiate both versions:

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

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

This can be compiled separately. The client code only needs the declaration of the template.

I'd personaly just stick with what Andy Prowl suggests, though.

于 2013-07-14T22:03:38.560 回答
5
// 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)
}
于 2018-04-03T04:36:38.600 回答
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"};}

请注意,这将适用于任何对其工作有意义的东西,而不仅仅是ostreams。这是一件好事。

如果函数调用的参数没有意义,它会简单地忽略这个定义。顺便说一句,如果缩进设置为 4 个空格,效果会更好。:)


这与 Cube 的答案相同,除了我说的是,如果可能的话,检查特定类型并让泛型编程来做它的事情会更优雅。

于 2018-04-04T07:04:16.627 回答
4

这是一个可以扩展到任意数量的参数并且不需要接受函数是模板的解决方案。

#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;
}

与其他解决方案相比,我更喜欢这个解决方案,因为它是惯用的,它不需要每次你想使用它时都编写函数模板,并且它会产生合理的编译器错误,例如......</p>

    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))
      |                       ^

锦上添花的是,该方案还支持用户自定义转换构造函数和转换运算符的隐式应用……</p>

#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));
}
于 2020-04-26T08:05:45.113 回答
0

如果我希望函数拥有函数参数的所有权,我倾向于将参数作为一个值,然后将其移入。如果参数的移动成本很高(例如 std::array),这是不可取的。

一个典型的例子是设置一个对象的字符串成员:

class Foo {
   private:
      std::string name;
   public:
      void set_name( std::string new_name ) { name = std::move(new_name); }
};

通过函数的这个定义,我可以调用 set 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; }
};

这对构造函数特别有用。

于 2019-03-28T10:26:34.473 回答