Function that accepts both lvalue and rvalue arguments

后端 未结 6 1079
长发绾君心
长发绾君心 2020-12-08 13:13

Is there a way to write a function in C++ that accepts both lvalue and rvalue arguments, without making it a template?

For example, suppose I write a function

相关标签:
6条回答
  • 2020-12-08 13:47
    // 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)
    }
    
    0 讨论(0)
  • 2020-12-08 13:48

    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.

    0 讨论(0)
  • 2020-12-08 13:53

    If I expect the function to take ownership of the argument of the function, I tend to put the argument as a value, and then move it in. This is not desirable if the argument is expensive to move (e.g. std::array).

    A typical example is setting an object's string member:

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

    With this definition of the function, I can call set the name with no copies of the string object:

    Foo foo;
    foo.set_name( std::string("John Doe") );
    // or
    std::string tmp_name("Jane Doe");
    foo.set_name( std::move(tmp_name) );
    

    But I can create a copy it if I want to keep the ownership of the original value:

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

    This last version would have very similar behavior to passing const reference and making a copy assignment:

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

    This is specially useful for constructors.

    0 讨论(0)
  • 2020-12-08 14:02

    Be bold, embrace generic forward functions and name them well.

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

    Note that this will work with anything that will make sense for it to work, not only ostreams. That is a good thing.

    If the function is called with an argument that doesn't make sense, it will simply ignore this definition. Incidentally, this works better if indentation is set to 4 spaces. :)


    This is the same as Cube's answer, except that I am saying that it is, when possible, more elegant to not check for specific types and let generic programming do its thing.

    0 讨论(0)
  • 2020-12-08 14:07

    Here's a solution that scales to any number of parameters and doesn't require the accepting function to be a template.

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

    I prefer this solution to the others because it's idiomatic, it doesn't require writing a function template every time you want to use it, and it produces sensible compiler errors, such as…

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

    The icing on the cake is that this solution also supports the implicit application of user-defined conversion constructors and conversion operators…

    #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));
    }
    
    0 讨论(0)
  • 2020-12-08 14:09

    There is not much of a sane choice other than offering two overloads or making your function a template, I would say.

    If you really, really need an (ugly) alternative, then I guess the only (insane) thing you can do is to have your function accept a const&, with a pre-condition saying that you cannot pass an object of a const-qualified type to it (you don't want to support that anyway). The function would then be allowed to cast away the constness of the reference.

    But I'd personally write two overloads and define one in terms of the other, so you do duplicate the declaration, but not the definition:

    void foo(X& x) 
    { 
        // Here goes the stuff... 
    }
    
    void foo(X&& x) { foo(x); }
    
    0 讨论(0)
提交回复
热议问题