views:

175

answers:

8

When looking over the member functions of the STL containers, an odd thought occurred to me. Why don't functions like std::vector<T>::push_back(T) not have an (optional) return value (iterator or even a reference to the appended object)? I know std::string functions like insert and erase return iterators, but that's for obvious reasons. I'd think it'd often save a second line of code that often follows these function calls.

I'm sure the designers of C++ have a very good reason, please enlighten me :)

UPDATE: I'm including a real-world code example here where it could reduce code length:

if( m_token != "{" )
{
    m_targets.push_back( unique_ptr<Target>(new Dough(m_token)) );
    return new InnerState( *(m_targets.back()), this );
}

could be reduced to

if( m_token != "{" )
    return new InnerState( *(m_targets.push_back( unique_ptr<Target>(new Dough(m_token)) )), this );

If I assume std::list::push_back returns a reference to the added element. The code is a bit heavy, but that's mostly (two sets of parentheses) due to unique_ptr's constructor and dereferencing it. Perhaps for clarity a version without any pointers:

if( m_token != "{" )
{
    m_targets.push_back( Dough(m_token) );
    return new InnerState( m_targets.back(), this );
}

vs.

if( m_token != "{" )
    return new InnerState( m_targets.push_back( Dough(m_token) ), this );
A: 

Not sure they had a very good reason, but this function is slow enough already.

Pavel Radzivilovsky
How so, "slow"? and what difference would a return value make?
anon
since return value is mostly returned in some register there's actually a chance it silently already returns a pointer to the inserted element. And it would not make much of a difference since vector<T>::back() is constant so an iterator(or pointer) must be saved by the vector anyway.
flownt
Slow compared to what?
GMan
+5  A: 

Interesting question. The obvious return value would be the vector (or whatever) that the operation takes place on, so you could then write code like:

if ( v.push_back(42).size() > n ) {
   // do something
}

I personally don't like this style, but I can't think of a good reason not to support it.

anon
I was thinking more in the lines of `element = v.push_back(i); element.somefunction(someVariable);`, but yours would work as well of course.
rubenvb
A: 

I think it has to do with the concept of a return value: the return value is there not for your convenience but for a conceptual result of the 'computation' they apparently thought push_back conceptually doesn't result in anything.

flownt
+3  A: 

Because there's .back() that will instantly return it for you?

Conceptually, the C++ designers won't implement in a member function anything that would be difficult or impossible for you to implement in a public interface. Calling .back() is simple and easy enough. For an iterator, you could do (end - 1) or just auto it = end; it--;

The Standards committee makes new code possible and massively simplifies code that is very commonly used. Stuff like this just isn't on the list of things to do.

DeadMG
Precision: make sure that there is an element in the container before calling `--` on `end`. Otherwise it's undefined behavior.
Matthieu M.
That's the same restriction as `.back()` has, and a postcondition of push_back() succeeding (i.e. not throwing)
MSalters
A: 

I am not sure, but I think that one of the reasons why the mutating std::string members return an iterator is so that the programmer could obtain a non-const-iterator to the std::string after a mutating operation without requiring a second "leak".

The std::basic_string interface was designed to support a pattern called copy-on-write, which basically means that any mutating operation does not affect the original data, but a copy. For example, if you had the string "abcde" and replaced 'a' with 'z' to get "zbcde", the data for the resulting string might occupy a different location in the heap than the data for the original string.

If you obtain a non-const-iterator of a std::string, then a COW string implementation must make a copy (also called "leak the original"). Otherwise, the program can change the underlying data (and violate the read-only invariant) with:

char& c0 = *str.begin();
c0 = 'z';

But, after a string mutation operation, the resulting string object already has sole ownership of the data, so the string implementation does not need to leak its data a second time to make a non-const-iterator.

A std::vector is different because it does not support copy-on-write semantics.

Note: I got the term leak from the libstdc++ implementation of std::basic_string. Also, "leaking the data" does not mean that the implementation leaks memory.

EDIT: Here is the libstdc++ definition of std::basic_string<CharT, Traits, Alloc>::begin() for reference:

iterator
begin()
{
    _M_leak();
    return iterator(_M_data());
}
Daniel Trebbien
+2  A: 
v.insert(v.end(),x);

Would be equivalent to push_back with returning an iterator. Why push_back itself doesn't return an iterator is beyond me.

5ound
A: 

Maybe because it was not "needed"?

erase() and insert() have no other way than return an iterator to allow continuing a loop it was called in.

I don't see a good reason to support the same logic with push_back().

But sure, it would be be wonderful to make more cryptic expressions. (I don't see an improvement in your example, it looks like a good way to slow your coworkers when reading your code...)

Klaim
+2  A: 

Returning the added element, or the container in container member functions is not possible in a safe way. STL containers mostly provide the "strong guarantee". Returning the manipulated element or the container would make it impossible to provide the strong guarantee (it would only provide the "basic guarantee"). The reason behind this is, that returning something could possibly invoke an copy-constructor, which may throw an exception. But the function already exited, so it fulfilled its main task successfully, but still threw an exception, which is a violation of the strong guarantee. You maybe think: "Well then lets return by reference!", while this sounds like a good solution, its not perfectly safe either. Consider following example:

MyClass bar = myvector.push_back(functionReturningMyClass()); // imagine push_back returns MyClass&

Still, if the copy-assignment operator throws, we dont know if push_back succeded or not, thus indirectly violating the strong-guarantee. Even though this is not a direct violation. Of course using MyClass& bar = //... instead would fix this issue, but it would be quite inconvenient, that a container might get into an indeterminate state, just because someone forgot a &.

A quite similar reasoning is behind the fact that std::stack::pop() does not return the popped value. Instead top() returns the topmost value in a safe way. after calling top, even when a copy-constructor, or a copy-assignment constructor throws, you still know that the stack is unchanged.

EDIT: I believe returning an iterator for the newly added element should be perfectly safe, if the copy-constructor of the iterator-type provides the no-throw guarantee (and every i know of does).

smerlin
Wow. Great info, thanks! Still leaves me wondering why they didn't return an iterator of course :). Perhaps because you could then `MyClass bar = *(myvector.push_back(functionReturningMyClass()));` and probably have the same problem as with returning a reference (or not?).
rubenvb
i guess, the reason were performance issues, since that thing would require construction of an iterator, and copying that iterator, and thats quite an overhead, if the return value is not used.
smerlin
@smerlin: wouldn't any decent compiler optimize such a thing away? Probably not what the standards committee was looking at :)
rubenvb