1

Context

When passing arguments to functions, we can either pass-by-reference or pass-by-value.

One advantage to passing by reference is that the argument does not need to be implemented via a copy of itself, and this can seem advantageous for custom objects that contain a lot of data.

But for multiple class interfaces, creating a pass-by-reference function for each interface can be… untidy e.g.:

void parse(Console&);
void parse(Dialog&);
void parse(Image&);
void parse(Window&);
void parse(...); // how many more overloads?!

So, on to templates to solve this untidy problem, right?

template <typename type> void parse(type&);

But I've hit a road bump…


Attempted Solution (Code)

#include <iostream>

class Object { public:
    // Copy/ Move constructors implicitly defined;
    //   Same goes for the assignment operator.
    constexpr inline Object(void) {}

    // Test if the object can be a reference or is an expression value.
    template <typename type>
    inline static void parse(type) noexcept { std::cout << "[pass-by-value]" << std::endl; }

    template <typename type>
    inline static void parse(type&) noexcept { std::cout << "[pass-by-reference]" << std::endl; }
};

Passing the object by value works

Object::parse(Object()); // SUCCESS: "pass-by-value"

but using a reference causes the test function to conflict with its overloads.

Object object {};
Object::parse(object); // ERROR: Ambiguous call of `Object::parse` function.
                       // EXPECTED: "pass-by-reference"

I assume the call is ambiguous because the
parse(type) overload is initialized with {type=Object&} and the
parse(type&) overload is initialized with {type=Object}.

Although, I thought that the parse(type&) overload would be preferred but apparently that doesn't seem to be the case.

So, what am I missing here?


Question

How can we differentiate between an argument that could be a reference or an argument that is a constant expression using a template function?

Note: I'm trying to prevent unnecessary copies with user-defined objects i.e.:

If I pass Image() [type=Image] as an argument, it will be passed by value.
Otherwise it will be passed by reference e.g.:

Image image; // -> image [type=Image&]
Image images[1]; // -> images[0] [type=Image&]
Image *image_p; // -> *(image_p + 0) [type=Image&]
Lapys
  • 936
  • 2
  • 11
  • 27
  • How exactly do you want to select the overload? – HolyBlackCat Mar 12 '20 at 21:49
  • @HolyBlackCat: The overload should be selected based on if the argument is a reference (to some variable or pointer) `Object object {};` or an inline expression `Object()` – Lapys Mar 12 '20 at 21:52
  • 1
    Have you considered using forwarding references (`T&&`). I'm not sure if I fully understood the question, so maybe it is not what you need. However, if you want to write code that works for all inputs and then calls another function, that would be the solution that comes to mind. – Philipp Claßen Mar 12 '20 at 21:52

2 Answers2

2

Basing on your comment, it seems that what you want is an overload on r-value reference (so that you can distinguish when temporary was passed or when existing object was passed)

void foo(int& x){
    std::cout << x << " was an l-value\n";
}

void foo(int&& x){
    std::cout << x << " was an r-value (possibly a temporary or std::move result)\n";
}

int main(){
    int x = 5;
    foo(x);
    foo(10);
}

Note that second version does not perform a copy either. A temporary object is constructed and then move semantics are used.


If you don't want to modify the original object and don't care about distinction, you can simply make argument a const reference

void foo(const int& x){
    std::cout << x << " may or may not have been a temporary "
                      "(it's lifetime is prolonged until end of this function)\n";
}


int main(){
    int x = 5;
    foo(x);
    foo(10);
}

No copies are needed here either.

Yksisarvinen
  • 18,008
  • 2
  • 24
  • 52
  • Guess I need to brush up my C++ on move semantics. Thanks, this is what I was looking for. – Lapys Mar 12 '20 at 22:07
  • 1
    @Lapys We have [a list of good C++ books](https://stackoverflow.com/questions/388242/the-definitive-c-book-guide-and-list) to help with that :) Make sure you get something that teaches C++11. – Yksisarvinen Mar 12 '20 at 22:09
  • `int&&` matches rvalues, which is an expression category, it does not necessarily designate a temporary. E.g. `foo(std::move(x))` – M.M Mar 13 '20 at 01:26
  • @M.M Changed wording. – Yksisarvinen Mar 13 '20 at 09:16
1

This is a somewhat common problem.

You could deffirantiate the methods according to size of the object and whether it's trivially copyable. If it's not trivially copyable or too large use const reference - otherwise use copy.

This way you have no ambiguity to resolve - just utilize SFINEA to enable/disable certain functions.

ALX23z
  • 4,456
  • 1
  • 11
  • 18
  • I assume you mean the `std::is_reference` and `std::is_value` meta functions? – Lapys Mar 12 '20 at 21:59
  • 1
    @Lapys, I mean if size is bigger than 128 then enable reference version. Otherwise enable copy version. 128 is rather arbitrary. If type is not trivially copyable - you ought to use reference version. – ALX23z Mar 12 '20 at 22:01
  • Is there a way to do this without SFINAE, though? – Lapys Mar 12 '20 at 22:03
  • 1
    @Lapys SFINEA is not complicated. Just nasty syntax. – ALX23z Mar 12 '20 at 22:03
  • @Lapys there are input types that can be distinguished and some cannot be. You just need to decide what's your input type is. Normally, its RValue reference and const reference for most types. – ALX23z Mar 12 '20 at 22:08
  • I'm guessing that's why copy & move constructors are implicitly defined for user-defined classes, right (or is that a whole other ball game)? – Lapys Mar 12 '20 at 22:10
  • @Lapys, yeah those two should be defined for basic types. For trivially copyable just default them - const reference should suffice. – ALX23z Mar 12 '20 at 22:15