2

I want to pass a std::list as a parameter to fn(std::list<int>), so I do fn({10, 21, 30}) and everybody is happy.

However, I've come to learn that one shouldn't pass list by value, cause it's costly. So, I redefine my fn as fn(std::list<int> &). Now, when I do the call fn({10, 21, 30}), I get an error: candidate function not viable: cannot convert initializer list argument to 'std::list<int> &'.

QUESTION TIME

  1. Is the "you shall not pass an costly object by value" rule valid here? We aren't passing a list after all, but an initializer_list, no?
  2. If the rule still applies, what's the easy fix here?

I guess my doubt comes from the fact that I don't know clearly what happens when one passes an initializer_list argument to a function that accepts a list.

  1. Is list generated on the spot and then passed by value? If not, what is it that actually happens?
doplumi
  • 2,938
  • 4
  • 29
  • 45
  • `{10, 21, 30}` is a temporary (rvalue) and wont bind to a non-const reference. try adding `const` – sp2danny Feb 29 '16 at 09:58
  • `fn({10, 21, 30})` calls your method with a rvalue reference, so the first method is ok since you take it as a value. The second method however takes a normal reference which will fail, since it's an rvalue reference you provide and not a valid object. – Marian Feb 29 '16 at 09:58
  • Use [forwarding](http://www.google.nl/url?sa=t&rct=j&q=&esrc=s&source=web&cd=4&ved=0ahUKEwjLhuj22pzLAhVHvQ8KHarnB2gQFghAMAM&url=http%3A%2F%2Fwww.cplusplus.com%2Freference%2Futility%2Fforward%2F&usg=AFQjCNFj6DiiNpPgOvL8eE3nJqIInyBibw&bvm=bv.115339255,d.ZWU) – Neijwiert Feb 29 '16 at 09:58
  • @Neijwiert: what would forwading do, exactly? restore the rvalue to a rvalue? – sp2danny Feb 29 '16 at 10:00
  • Since your initializer list is no longer accessible to your program after you pass it, it is safe to forward it. It just moves pointers and does no copying. This is really cheap and fast. Google some C++11 forwarding, other people can explain this better. – Neijwiert Feb 29 '16 at 10:01
  • And since std::list already implements a move constructor you just have to change your function to this: `fn(std::list &&)`. – Neijwiert Feb 29 '16 at 10:02
  • You may check out [this](http://stackoverflow.com/questions/3279543/what-is-the-copy-and-swap-idiom/3279550#3279550) or [this](http://stackoverflow.com/questions/3106110/what-are-move-semantics/3109981#3109981). Both explain forwarding as well. – Marian Feb 29 '16 at 10:04
  • It is a very weird concept to grasp, but it is very useful if you know how it works. Can speed up costly operations a lot. – Neijwiert Feb 29 '16 at 10:05
  • First, you are not passing an initializer_list. you're using a braced init list, which leads to copy-list-initialization, that in the case of a reference parameter, creates a prvalue temporary. Otherwise (if passed "by value"), no temporary list is created – Piotr Skotnicki Feb 29 '16 at 10:06
  • forwarding restore the value category. this is useful if you have bound a temporary to a rvalue reference. `{1,2,3}` is already a rvalue. forwarding is not appropriate here. – sp2danny Feb 29 '16 at 10:09
  • @sp2danny *"{10, 21, 30} is a temporary (rvalue)"*, what *type* is that temporary ? – Piotr Skotnicki Feb 29 '16 at 10:09
  • The info you seem to be missing is that when you call a function, the parameter is initialized from the argument. In your second example it is the same as `list &arg = {10, 20, 30};` (which is ill-formed; when a non-const lvalue reference is initialized, it must directly bind to a glvalue) – M.M Feb 29 '16 at 12:20

6 Answers6

3

However, I've come to learn that one shouldn't pass list by value, cause it's costly.

That's not entirely accurate. If you need to pass in a list that the function can modify, where the modifications shouldn't be externally visible, you do want to pass a list by value. This gives the caller the ability to choose whether to copy or move from an existing list, so gives you the most reasonable flexibility.

If the modifications should be externally visible, you should prevent temporary list objects from being passed in, since passing in a temporary list object would prevent the caller from being able to see the changes made to the list. The flexibility to silently pass in temporary objects is the flexibility to shoot yourself in the foot. Don't make it too flexible.

If you need to pass in a list that the function will not modify, then const std::list<T> & is the type to use. This allows either lvalues or rvalues to be passed in. Since there won't be any update to the list, there is no need for the caller to see any update to the list, and there is no problem passing in temporary list objects. This again gives the caller the most reasonable flexibility.

Is the "you shall not pass an costly object by value" rule valid here? We aren't passing a list after all, but an initializer_list, no?

You're constructing a std::list from an initializer list. You're not copying that std::list object, but you are copying the list items from the initializer list to the std::list. If the copying of the list items is cheap, you don't need to worry about it. If the copying of the list items is expensive, then it should be up to the caller to construct the list in some other way, it still doesn't need to be something to worry about inside your function.

If the rule still applies, what's the easy fix here?

Both passing std::list by value or by const & allow the caller to avoid pointless copies. Which of those you should use depends on the results you want to achieve, as explained above.

Is list generated on the spot and then passed by value? If not, what is it that actually happens?

Passing the list by value constructs a new std::list object in the location of the function parameter, using the function argument to specify how to construct it. This may or may not involve a copy or a move of an existing std::list object, depending on what the caller specifies as the function argument.

  • And since we're talking about C++11 here, move semantics come into play. An `std::list` is being constructed from a temporary, which is then passed into a function by value. The compiler will try to move that into the function parameter, so you're not paying for a copy anyway. – Andre Kostur Feb 29 '16 at 16:15
0
  1. You can have std::list<> because in fact you're making temporary list and passing initializer_list by value is cheap. Also accessing that list later can be faster than a reference because you avoid dereferencing.

  2. You could hack it by having const& std::list as parameter or like that

    void foo( std::list<int> &list ) {}
    
    int main() {
        std::list<int> list{1,2,3};
    
        foo( list );
    }
    
  3. List is created on function scope and this constructor is called

    list (initializer_list<value_type> il,
          const allocator_type& alloc = allocator_type())
    

So there's no passing list by value. But if you'll use that function and pass list as parameter it'll be passed by value.

doplumi
  • 2,938
  • 4
  • 29
  • 45
stryku
  • 722
  • 5
  • 12
  • Using a reference does not imply dereferencing. It is just an alias. – Captain Giraffe Feb 29 '16 at 10:14
  • Since when is making a 'temporary list and passing initializer_list by value' cheap? You assume it is small and even so I'm pretty sure moving it is still faster. – Neijwiert Feb 29 '16 at 10:16
  • @PiotrSkotnicki I'm saying that if you would implement it with move semetatics it would probably be faster – Neijwiert Feb 29 '16 at 10:22
  • @Neijwiert by taking an rvalue reference parameter ? how would that be faster ? if the parameter is of a reference type, then a prvalue temporary is created. if it's taken "by value", then the appriorpate constructor, taking `std::initializer_list` is selected – Piotr Skotnicki Feb 29 '16 at 10:23
  • @PiotrSkotnicki You're right it depends on the situation. But it would allow you to use the initializer list in the function call. – Neijwiert Feb 29 '16 at 10:26
  • @CaptainGiraffe I couldn't answer better than [this](http://stackoverflow.com/questions/17803475/why-is-stdinitializer-list-often-passed-by-value#comment25975808_17803511) – stryku Feb 29 '16 at 10:27
  • @Neijwiert Yes because in most cases it's small. It's usually pair of pointers. – stryku Feb 29 '16 at 10:27
0

The expression {10, 21, 30} will construct a initializer_list<int>
This in turn will be used to create a list<int>
That list will be a temporary and a temporarys will not bind to a
non-const reference.

One fix would be to change the prototype for you function to
fn(const std::list<int>&)

This means that you can't edit it inside the function, and you probably don't need to.
However, if you must edit the parameter inside the function, taking it by value would be appropriate.

Also note, don't optimize prematurely, you should always use idiomatic constructs that clearly represents what you want do do, and for functions, that almost always means parameters by const& and return by value.

This is easy to use right, hard to use wrong, and almost always fast enough.

Optimization should only be done after profiling, and only for the parts of the program that you have measured to need it.

sp2danny
  • 7,488
  • 3
  • 31
  • 53
0

Quoting the C++14 standard draft, (emphasis are mine)

18.9 Initializer lists [support.initlist]

2: An object of type initializer_list provides access to an array of objects of type const E. [ Note: A pair of pointers or a pointer plus a length would be obvious representations for initializer_list. initializer_list is used to implement initializer lists as specified in 8.5.4. Copying an initializer list does not copy the underlying elements. —end note ]

std::list has a constructor which is used to construct from std::initializer_list. As you can see, it takes it by value.

list(initializer_list<T>, const Allocator& = Allocator());

If you are never going to modify your parameter, then fn(const std::list<int>&) will do just fine. Otherwise, fn(std::list<int>) will suffice well for.

To answer your questions:

Is the "you shall not pass an costly object by value" rule valid here? We aren't passing a list after all, but an initializer_list, no?

std::initializer_list is not a costly object. But std::list<int> surely sounds like a costly object

If the rule still applies, what's the easy fix here?

Again, it's not costly

Is list generated on the spot and then passed by value? If not, what is it that actually happens?

Yes, it is... your list object is created on the spot at run-time right before the program enters your function scope

WhiZTiM
  • 21,207
  • 4
  • 43
  • 68
  • Just to be clear: the list isn't generated by the caller function, right? – doplumi Feb 29 '16 at 12:39
  • Using `std::initializer_list` means he knows how many objects he wants to put in there at compile time, so, `fn({1, 4, 7})` : its the caller that generates the `initializer_list` as an `rvalue`, which binds to the callee's parameter, who then copies it – WhiZTiM Feb 29 '16 at 12:58
0

However, I've come to learn that one shouldn't pass list by value, cause it's costly. So, I redefine my fn as fn(std::list &). Now, when I do the call fn({10, 21, 30}), I get an error: candidate function not viable: cannot convert initializer list argument to 'std::list &'.

A way to fix the problem would be:

fn(std::list<int>& v) {
   cout << v.size();
}

fn(std::list<int>&& v) {
    fn(v);
}

Now fn({1, 2, 3 }); works as well (it will call the second overloaded function that accepts a list by rvalue ref, and then fn(v); calls the first one that accepts lvalue references.

fn(std::list<int> v)
{

}

The problem with this function is that it can be called like:

list<int> biglist;
fn(biglist);

And it will make a copy. And it will be slow. That's why you want to avoid it.

I would give you the following solutions:

  1. Overloaded your fn function to accept both rvalues and lvalues properly as shown before.
  2. Only use the second function (the one that accepts only rvalue references). The problem with this approach is that will throw a compile error even if it's called with a lvalue reference, which is something you want to allow.
Jts
  • 3,447
  • 1
  • 11
  • 14
  • `fn(list{10,21,30})` will not work when the prototype is `fn(std::list &)`. It's also not different in meaning from `fn({10, 21, 30})` in this context. – sp2danny Feb 29 '16 at 10:50
  • I actually didn't know that, the code works perfectly fine in VS 2015, I didn't know it wasn't legal. – Jts Feb 29 '16 at 10:59
0

Like the other answers and comments you can use a const reference to the list.

void fn(const std::list<int>& l)
{
    for (auto it = l.begin(); it != l.end(); ++it)
    {
        *it; //do something
    }
}

If this fn function is heavily used and you are worried about the overhead of constructing and destructing the temporary list object, you can create a second function that receives the initializer_list directly that doesn't involve any copying whatsoever. Using a profiler to catch such a performance hot spot is not trivial in many cases.

void fn(const std::initializer_list<int>& l)
{
    for (auto it = l.begin(); it != l.end(); ++it)
    {
        *it; //do something
    }
}
egur
  • 7,830
  • 2
  • 27
  • 47