By Andrew Tomazos


2019-04-14 11:37:58 8 Comments

Suppose I want to write a function that calls a nullary function 100 times. Which of these implementations is best and why?

template<typename F>
void call100(F f) {
    for (int i = 0; i < 100; i++)
        f();
}

template<typename F>
void call100(F& f) {
    for (int i = 0; i < 100; i++)
        f();
}

template<typename F>
void call100(const F& f) {
    for (int i = 0; i < 100; i++)
        f();
}


template<typename F>
void call100(F&& f) {
    for (int i = 0; i < 100; i++)
        f();
}

Or is there a better implementation?

Update regarding 4

struct S {
    S() {}
    S(const S&) = delete;
    void operator()() const {}
};

template<typename F>
void call100(F&& f) {
    for (int i = 0; i < 100; i++)
        f();
}

int main() {
    const S s;
    call100(s);
}

3 comments

@Marshall Clow 2019-04-14 11:47:46

I would use the first one (pass the callable by value).

If a caller is concerned about the cost of copying the callable, then they can use std::ref(f) or std::cref(f) to pass it using reference_wrapper.

By doing this, you provide the most flexibility to the caller.

@Hoodi 2019-04-14 12:02:21

But in the main code of the question, it JUST calls the f 100 times. No?

@Artyer 2019-04-14 12:18:15

Also note this is how the standard library does it in the algorithmns library (e.g. std::for_each, std::count_if)

@Barry 2019-04-14 17:32:59

@Artyer Although those predate forwarding references.

@Resurrection 2019-04-14 12:06:43

I do not think there is a definitive answer:

  1. The first one copies everything you pass in which might be expensive for capturing lambdas but otherwise provides the most flexibility:

    Pros

    • Const objects allowed
    • Mutable objects allowed (copied)
    • Copy can be elided (?)

    Cons

    • Copies everything you give it
    • You cannot call it with an existing object such as mutable lambda without copying it in
  2. The second one cannot be used for const objects. On the other hand it does not copy anything and allows mutable objects:

    Pros

    • Mutable objects allowed
    • Copies nothing

    Cons

    • Does not allow const objects
  3. The third one cannot be used for mutable lambdas so is a slight modification of the second one.

    Pros

    • Const objects allowed
    • Copies nothing

    Cons

    • Cannot be called with mutable objects
  4. The fourth one cannot be called with const objects unless you copy them which becomes quite awkward with lambdas. You also cannot use it with pre-existing mutable lambda object without copying it or moving from it (losing it in the process) which is similar limitation to 1.

    Pros

    • Avoids copies explicitely by forcing (requiring) move semanthics if the copy is needed
    • Mutable objects allowed.
    • Const objects allowed (except for mutable lambdas)

    Cons

    • Does not allow const mutable lambdas without a copy
    • You cannot call it with an existing object such as mutable lambda

And there you have it. There is no silver bullet here and there are different pros & cons to each of these versions. I tend to lean towards the first one being the default but with certain types of capturing lambdas or bigger callables, it might become an issue. And you cannot call the 1) with the mutable object and get an expected result. As mentioned in the other answer some of these can be overcome with std::ref and other ways of manipulating the actual T type. In my experience, these tend to be the source of pretty nasty bugs though when T is then something different than one expects to achieve i.e. mutability of a copy or such.

@Andrew Tomazos 2019-04-14 12:12:58

I don't think your analysis of 4 is correct. Passed a const value won't F be deduced as const T& and be passed by reference?

@Andrew Tomazos 2019-04-14 12:14:54

See "Update regarding 4"

@Resurrection 2019-04-14 12:15:06

@AndrewTomazos Not with const mutable lambdas. Or rather it will deduce it as you say but would refuse to compile because it would discard the const at the call site. Using latest MSVC2017, not sure about Clang/GCC as Godbolt seems not to work atm.

@Yakk - Adam Nevraumont 2019-04-14 12:50:52

@artyr No, that is nonsense. Feel free to test it yourself, but 4 won't call operator()&&.

@Yakk - Adam Nevraumont 2019-04-14 12:49:19

The only runtime cost of

template<typename F>
void call100(F&& f) {
  for (int i = 0; i < 100; ++i)
    f();
}

is that it can have more versions (copies of code) if you pass f in multiple ways. With MSVC or the gold linker with ICF, those copies only cost compile time unless they differ, and if they differ you probably want to keep them.

template<typename F>
void call100(F f) {
  for (int i = 0; i < 100; ++i)
    f();
}

this one has the advantage of being value semantics; and following the rule of taking values unless you have good reason not to is reasonable. std::ref/std::cref let you call it with a persistant reference, and for prvalues guaranteed elision will prevent a spurious copy.

As a joke you could do:

template<typename F>
void call100(F&& f) {
  for (int i = 0; i < 99; ++i)
    f();
  std::forward<F>(f)();
}

but that relies on people having && overloads on their operator(), which nobody does.

Related Questions

Sponsored Content

21 Answered Questions

[SOLVED] What is the "-->" operator in C++?

10 Answered Questions

27 Answered Questions

[SOLVED] Easiest way to convert int to string in C++

35 Answered Questions

12 Answered Questions

[SOLVED] How can I profile C++ code running on Linux?

  • 2008-12-17 20:29:24
  • Gabriel Isenberg
  • 457878 View
  • 1629 Score
  • 12 Answer
  • Tags:   c++ unix profiling

13 Answered Questions

[SOLVED] What is the effect of extern "C" in C++?

23 Answered Questions

[SOLVED] Why do we need virtual functions in C++?

1 Answered Questions

[SOLVED] The Definitive C++ Book Guide and List

  • 2008-12-23 05:23:56
  • grepsedawk
  • 2109400 View
  • 4249 Score
  • 1 Answer
  • Tags:   c++ c++-faq

7 Answered Questions

Sponsored Content