1

Parameters can be taken in many different ways:

void foo(T obj);
void foo(const T obj);
void foo(T& obj);
void foo(const T& obj);
void foo(T&& obj);
void foo(const T&& obj);

I didn't include taking a parameter via a pointer because a pointer could already be the type T.

The first two ways are taking the object by value which will cause the object to get copied (if not optimized by a smart compiler).

The other ways take the object by reference (the first two by a l-value reference and the last two as a r-value reference) which should omit making any copies. The const qualified versions promise not to modify the referenced object.

If I want my function to be able to take as parameters all kinds of values (variables, temporaries returned from functions, literals etc.) what should be its prototype for a not-object-modifying version (const) and a potentially-object-modifying version (not const) version, if I want it to be the most efficent (avoid invoking copy constructors, move constructors, assignment operators, creating temporaries etc.)?

I'm not sure which type of references should I use. If this is a subjective question I am looking for pros and cons of each approach.

  • 4
    This is a very broad and subjective question, and depends a lot on the context, design and use-case. There's no true "right" or "wrong" way to do this. – Some programmer dude Jul 03 '19 at 11:31
  • When I have pointers or small objects like `int`, `double` I pass it by `void foo(T obj);` or `void foo(const T obj);`, in other cases I use `void foo(T& obj);` or `void foo(const T& obj);` – Raffallo Jul 03 '19 at 11:33
  • 1
    *avoid invoking copy constructors, move constructors, assignment operators, creating temporaries etc* -- For certain types, you will still incur construction, even if the parameter is a reference. For example, for `const T&`, the `T` is a `std::string`, and you pass a string-literal like `"abc"`. – PaulMcKenzie Jul 03 '19 at 11:35
  • As @Someprogrammerdude said, it really depends. For example, if my function worked with a string i'd make the function argument an `std::string_view`. If my function worked with a container (like a `vector`) my function argument would be a `span` (C++20) or iterators. If I wanted a `readonly` variable I'd probably take it as a `T const&` assuming the parameter wasn't something like an `int` for example. If I know I'll be `moving` a variable into the function i'll take it by value.. There really is no single, default way to do this – WBuck Jul 03 '19 at 11:57
  • The first two are the same; the compiler ignores top-level `const` in argument lists in function declarations. In a function **definition** the `const` prevents modifying `obj`. – Pete Becker Jul 03 '19 at 12:58
  • @PeteBecker So cv-qualifiers *must* be present in the function definition to actually apply? –  Jul 03 '19 at 13:01
  • @YanB. -- yes, top-level cv-qualifiers on arguments only matter in a function **definition**. But let's not go further down that rabbit hole; I probably shouldn't have mentioned it in the first place. – Pete Becker Jul 03 '19 at 13:03

2 Answers2

5

There is a reason why all these combinations for parameter declaration exist. It just depends on what you need:

  • void foo(T obj);
    • the caller doesn't want obj to be modified [so it's copied]
    • foo can modify obj [it's a copy]
    • if foo doesn't modify obj this is still prefered over const T& - assuming T is small (fits 1-2 CPU registers)

  • void foo(const T obj);
    • the caller doesn't want obj to be modified [so it's copied]
    • foo can't modify obj
    • remember, const is often to help you find errors. That's why this is generally used to avoid accidentely modifying obj. (e.g. if (obj = 5))

  • void foo(T& obj);
    • the caller expects a change to obj
    • foo can modify obj
    • a reference means no copying. So this is useful for passing expensive-to-copy objects. However, this could be slow for small types (int, double etc.) meaning a pass-by-copy would be better.

  • void foo(const T& obj);
    • the caller doesn't want obj to be modified
    • foo can't modify obj
    • a reference means no copying. Since it's unmodifiable, this is the perfect way to parameterize large read-only containers. So: very cheap for expensive-to-copy objects, could be slow for small types.

  • void foo(T&& obj);
    • the caller doesn't care what happens to obj and has no problems if it's empty afterwards
    • foo can modify obj by stealing the data by moving the information to another place.

  • void foo(const T&& obj);
    • foo can't modify obj, which makes this rarely useful
    • however, this disallows lvalues to be passed as arguments.

There are many special cases so this is in-no-way a complete list.


some extra bits:

  • copy-swap-idiom
  • (const T& obj) is often worse than just (T obj) for many reasons. But remember the caller can always let T simply be std::reference_wrapper<const T> to avoid copying. This could break the function, though.
  • even when you don't std::move there is a lot of moving going on - assuming the type has the necessary operators.
  • Functions / Lambdas? Pass by value: template <typename F> void execute(F f) { f(); }

Finally, worth sharing is this flow chart made by Bisqwit from this video which makes for a nice graphic:

parameter declaration flow chart

Stack Danny
  • 5,198
  • 1
  • 15
  • 42
0

Most efficient way to take parameters in modern C++?

The most efficient way depends on the type of the parameter and whether the parameter will be modified.

For types that will fit into the processor's register, passing by copy is the most efficient. (The compiler may choose to pass variables in registers rather than the stack.)

For larger types, including the cases when the parameter will be modified, passing by reference is efficient (and safe). Passing by pointer is efficient, but is not as safe as passing by reference.

For many parameters, consider placing them into a structure and passing the structure to the function (via reference or const reference).

The processor is most efficient when using data types that fit inside a register.

Note: This is a micro-optimization, generally reserved for time critical applications. Prefer to work on robustness and correctness before worrying about the efficiency of passing parameters.

Stack Danny
  • 5,198
  • 1
  • 15
  • 42
Thomas Matthews
  • 52,985
  • 12
  • 85
  • 144