After an evaluation, GNOME has moved from Bugzilla to GitLab. Learn more about GitLab.
No new issues can be reported in GNOME Bugzilla anymore.
To report an issue in a GNOME project, go to GNOME GitLab.
Do not go to GNOME Gitlab for: Bluefish, Doxygen, GnuCash, GStreamer, java-gnome, LDTP, NetworkManager, Tomboy.
Bug 756484 - Move operators in libsigc++
Move operators in libsigc++
Status: RESOLVED FIXED
Product: libsigc++
Classification: Bindings
Component: signals
2.6.x
Other All
: Normal normal
: ---
Assigned To: libsigc++ maintainer(s)
libsigc++ maintainer(s)
Depends on:
Blocks:
 
 
Reported: 2015-10-13 07:46 UTC by Kjell Ahlstedt
Modified: 2015-11-16 14:38 UTC
See Also:
GNOME target: ---
GNOME version: ---


Attachments
patch: slot and signal: Fix move constructors and move assignments (12.71 KB, patch)
2015-10-16 14:03 UTC, Kjell Ahlstedt
none Details | Review
patch: slot and signal: Add missing move constructors and move assignments (8.52 KB, patch)
2015-10-21 17:06 UTC, Kjell Ahlstedt
none Details | Review
patch: trackable, slot, signal: Remove noexcept specifications (5.86 KB, patch)
2015-11-06 14:04 UTC, Kjell Ahlstedt
committed Details | Review
patch: slot and signal: Add missing move constructors and move assignments (4.86 KB, patch)
2015-11-06 14:06 UTC, Kjell Ahlstedt
committed Details | Review
patch: Add a moving signal::connect() overload (6.58 KB, patch)
2015-11-12 10:07 UTC, Kjell Ahlstedt
committed Details | Review

Description Kjell Ahlstedt 2015-10-13 07:46:33 UTC
I doubt that all move constructors and move assignment operators in libsigc++
are correct.

CC = copy constructor
CA = copy assignment operator
MC = move constructor
MA = move assignment operator
!! = need more thought and/or fix

- trackable
  User-defined CC, CA, MC, MA. Ok after the fix of bug 755393.
  !! MC and MA are noexcept, although they call notify_callbacks(),
  which is not noexcept, and may throw an exception. Is that ok?

- slot_rep
  Compiler-generated CC, CA, MC, MA.
  !! Should be deleted. These operators are never used.

- typed_slot_rep
  User-defined CC. Called by dup(). Calls a user-defined slot_rep constructor
  (not CC).
  Compiler-generated CA.
  No MC, MA, because there is a user-defined CC, and no user-defined MC, MA.
  !! CA, MC, MA should be deleted.

- slot_base
  User-defined CC, CA, MC, MA.
  CC, CA ok.
  !! MC not ok. Not reasonable to move a connected slot_base.
     If src is connected (src.rep->parent_ != nullptr), then do like CC.
     If src is not connected, then add a call to src.rep_->notify_callbacks().
  !! MA not ok. Keep rep_->parent_ and rep_->cleanup_, like CA.
     If src is connected (src.rep_->parent_ != nullptr), then do like CA.
     If src is not connected, then add a call to src.rep_->notify_callbacks().

- slot1..slot7
  User-defined CC, CA, MC, MA. Ok.

- slot
  User-defined CC.
  Compiler-generated CA.
  No MC, MA, because there is a user-defined CC, and no user-defined MC, MA.
  !! Add user-defined CA, MC, MA.

- signal_impl
  Compiler-generated CC, CA, MC, MA.
  !! Should be deleted. signal_impl is never copied or moved.

- signal_base
  User-defined CC, CA, MC, MA.
  CC, CA, MC ok.
  !! MA not ok. No self-assignment guard. Old impl_ must be handled like in CA
     before it's reassigned. Shall it continue to call trackable::operator=()
     or call src.notify_callbacks() instead? I recommend src.notify_callbacks(),
     because CA does not call trackable::operator=(). MA should have the same
     effect on the destination as CA has.

- signal1..signal7
  User-defined CC.
  Compiler-generated CA.
  No MC, MA, because there is a user-defined CC, and no user-defined MC, MA.
  !! Add user-defined CA, MC, MA.

- signal
  User-defined CC.
  Compiler-generated CA.
  No MC, MA, because there is a user-defined CC, and no user-defined MC, MA.
  !! Add user-defined CA, MC, MA.


The test programs libsigc++2/tests/test_signal_move.cc and test_slot.move.cc
don't call any move operators. Neither signal<> nor slot<> has move operators.
Both the tests that are said to test move constructors and those that are said
to test move assignment actually call copy constructors.

Can we delete operators? Wouldn't it break API/ABI? No, not in this case.
slot_rep, typed_slot_rep and signal_impl are declared in the sigc::internal
namespace. They shall not be used directly by code outside libsigc++.

What about connected slots (slots with a parent)? It seems almost out of the
question to move a connected slot_rep from one slot_base to another. First, it
must be disconnected from its parent. The parent does not know or care about the
slot_rep. The parent knows about the corresponding slot. It's up to the parent
what to do when a slot is disconnected. The parent can for example delete the
slot, including the corresponding slot_rep. Usually a slot with a parent is
owned by the parent. It's possible that there is a solution that makes it
possible to move connected slots, but it's not worth the trouble.


Comments, before I fix the move operators?
Comment 1 Jonas Platte 2015-10-13 11:56:40 UTC
> although they call notify_callbacks(), which is not noexcept, and may throw an > exception. Is that ok?

That would mean that std::terminate is called when an exception is thrown there. I don't know whether that's ok though.
Comment 2 Kjell Ahlstedt 2015-10-16 14:03:57 UTC
Created attachment 313466 [details] [review]
patch: slot and signal: Fix move constructors and move assignments

This is what I believe should be done to the move operators.
In addition to the modifications listed in comment 0, I have removed "noexcept"
from slot's move operators. That's reasonable when they sometimes make a copy
instead of a move.

In text books move operations are simple. In libsigc++ they are not.
In text book examples an object, A, contains a pointer to a huge object, B.
B does not contain a pointer to A, neither directly nor indirectly via one or
more other objects. A move just comprises
  delete pb; // in assignment
  pb = src.pb;
  src.pb = nullptr;
In libsigc++ it's not that easy.
Comment 3 Kjell Ahlstedt 2015-10-21 13:51:15 UTC
I just realized that I should split my patch into two: one that fixes existing
move operators and can be included in libsigc++ 2.6.x, and another one that
adds missing move operators, i.e. adds new API, and must wait until 2.7.x.
Comment 4 Kjell Ahlstedt 2015-10-21 17:06:03 UTC
Created attachment 313819 [details] [review]
patch: slot and signal: Add missing move constructors and move assignments

I have pushed https://git.gnome.org/browse/libsigc++2/commit/?id=cabc88f6c2e25432a8e6245c5af114ebb0de04b8
with the fixes that don't add new API.

The attached patch contains added move operators. It can be pushed to the master
branch when the libsigc-2-6 branch is created. I can do it now, if someone wants
that new API now, otherwise it can wait a while.
Comment 5 Murray Cumming 2015-11-01 13:28:47 UTC
(In reply to Kjell Ahlstedt from comment #0)
> I doubt that all move constructors and move assignment operators in libsigc++
> are correct.

Many thanks for reviewing all this. Sorry for not finding the time until now to respond.

> CC = copy constructor
> CA = copy assignment operator
> MC = move constructor
> MA = move assignment operator
> !! = need more thought and/or fix
> 
> - trackable
>   User-defined CC, CA, MC, MA. Ok after the fix of bug 755393.
>   !! MC and MA are noexcept, although they call notify_callbacks(),
>   which is not noexcept, and may throw an exception. Is that ok?

No, it's not OK. Maybe we can make notify_callbacks() and any callbacks noexcept?
 
> - slot_rep
>   Compiler-generated CC, CA, MC, MA.
>   !! Should be deleted. These operators are never used.

Yes. Let's use = delete there.
I'm fine with doing this in the current branch. It's not going to be very disruptive, I guess.

> - typed_slot_rep
>   User-defined CC. Called by dup(). Calls a user-defined slot_rep constructor
>   (not CC).
>   Compiler-generated CA.
>   No MC, MA, because there is a user-defined CC, and no user-defined MC, MA.
>   !! CA, MC, MA should be deleted.

Yes, likewise.
 
> - slot_base
>   User-defined CC, CA, MC, MA.
>   CC, CA ok.
>   !! MC not ok. Not reasonable to move a connected slot_base.
>      If src is connected (src.rep->parent_ != nullptr), then do like CC.
>      If src is not connected, then add a call to
> src.rep_->notify_callbacks().
>   !! MA not ok. Keep rep_->parent_ and rep_->cleanup_, like CA.
>      If src is connected (src.rep_->parent_ != nullptr), then do like CA.
>      If src is not connected, then add a call to
> src.rep_->notify_callbacks().

Whenever we have logic in the MC and MA, it really seems like something we should test.
 
> - slot1..slot7
>   User-defined CC, CA, MC, MA. Ok.
> 
> - slot
>   User-defined CC.
>   Compiler-generated CA.
>   No MC, MA, because there is a user-defined CC, and no user-defined MC, MA.
>   !! Add user-defined CA, MC, MA.

You've pushed these. Great.

> - signal_impl
>   Compiler-generated CC, CA, MC, MA.
>   !! Should be deleted. signal_impl is never copied or moved.

Yes, as for other needed "= delete" mentions above.

> - signal_base
>   User-defined CC, CA, MC, MA.
>   CC, CA, MC ok.
>   !! MA not ok. No self-assignment guard. Old impl_ must be handled like in
> CA
>      before it's reassigned. Shall it continue to call trackable::operator=()
>      or call src.notify_callbacks() instead? I recommend
> src.notify_callbacks(),
>      because CA does not call trackable::operator=(). MA should have the same
>      effect on the destination as CA has.
> 
> - signal1..signal7
>   User-defined CC.
>   Compiler-generated CA.
>   No MC, MA, because there is a user-defined CC, and no user-defined MC, MA.
>   !! Add user-defined CA, MC, MA.
> 
> - signal
>   User-defined CC.
>   Compiler-generated CA.
>   No MC, MA, because there is a user-defined CC, and no user-defined MC, MA.
>   !! Add user-defined CA, MC, MA.

You've done these. Thanks.
 
> The test programs libsigc++2/tests/test_signal_move.cc and test_slot.move.cc
> don't call any move operators. Neither signal<> nor slot<> has move
> operators.
> Both the tests that are said to test move constructors and those that are
> said
> to test move assignment actually call copy constructors.
> 
> Can we delete operators? Wouldn't it break API/ABI? No, not in this case.
> slot_rep, typed_slot_rep and signal_impl are declared in the sigc::internal
> namespace. They shall not be used directly by code outside libsigc++.
> 
> What about connected slots (slots with a parent)? It seems almost out of the
> question to move a connected slot_rep from one slot_base to another. First,
> it
> must be disconnected from its parent. The parent does not know or care about
> the
> slot_rep. The parent knows about the corresponding slot. It's up to the
> parent
> what to do when a slot is disconnected. The parent can for example delete the
> slot, including the corresponding slot_rep. Usually a slot with a parent is
> owned by the parent. It's possible that there is a solution that makes it
> possible to move connected slots, but it's not worth the trouble.
> 
> 
> Comments, before I fix the move operators?

Let's take care of the easy stuff first.
Comment 6 Kjell Ahlstedt 2015-11-02 12:28:30 UTC
(In reply to Murray Cumming from comment #5)
> Maybe we can make notify_callbacks() and any callbacks noexcept?

We can make more functions noexcept, but it can't be made totally consistent,
because we don't have complete control over the callbacks. Some callbacks may
execute code from any package that uses libsigc++.

slot_rep::notify() will throw an exception, if either the callback set with
slot_base::set_parent(), or the destruction of the stored functor throws
an exception.

We could for instance mark noexcept:
  trackable::notify_callbacks()
  trackable::~trackable()
  trackable_callback_list::~trackable_callback_list()
  slot_rep::notify()
  connection::notify()
  destroy_notify_struct::notify()  // Local class in slot_base.cc

but only the last two are unconditionally non-throwing. All the other functions
may invoke code outside libsigc++. Would anything be gained by marking them
noexcept, although we can't give a guarantee?

We can add a note to the description of slot_base::set_parent() and
trackable::add_destroy_notify_callback(), saying that the program may be
terminated if the callback function throws an exception.

>> - slot
>>   !! Add user-defined CA, MC, MA.

> You've pushed these. Great.

No, I haven't. That's in the patch in comment 4.

>> - signal1..signal7
>>   !! Add user-defined CA, MC, MA.
>> 
>> - signal
>>   !! Add user-defined CA, MC, MA.

> You've done these. Thanks.

Likewise, not pushed yet.

The patch in comment 4 also removes "noexcept" from slot's move constructor and
move assignment. Do you think that's OK? I think it is, when they sometimes make
a copy instead of a move.
Comment 7 Jonas Platte 2015-11-02 17:39:42 UTC
I think it would be a bad idea to make functions noexcept that will execute an external callback. Is it not possible to use something like noexcept(noexcept(callback())) in these cases? (there is a noexcept *operator* in addition to the specifier, it's used to determine if an expression is noexcept: http://en.cppreference.com/w/cpp/language/noexcept)
Comment 8 Kjell Ahlstedt 2015-11-03 08:40:46 UTC
Can you explain in more detail what you have in mind? I don't understand how
noexcept(noexcept(callback())) can be used in libsigc++.

Whether trackable::notify_callbacks() can throw an exception or not depends on
which callbacks are registered with calls to trackable::
add_destroy_notify_callback() and slot_base::set_parent(). It also depends on
which function objects are stored in slots. If the destruction of such a
function object throws an exception, that exception can propagate through
slot_rep::notify() and trackable::notify_callbacks().
Comment 9 Jonas Platte 2015-11-03 10:09:37 UTC
Okay, so I had a look at the documentation. I should have done that before, sorry.

That said, I have an idea how a conditional noexcept would be possible, but it's a bit crazy, as it involves making trackable a template (with a boolean parameter for whether all of its callbacks are noexcept, defaulting to true). But I guess I can stop there as that would break everything in some way?
Comment 10 Kjell Ahlstedt 2015-11-05 12:03:21 UTC
The easiest fix concerning trackable's move operators would be to remove
"noexcept" from them. Is that acceptable?
Comment 11 Murray Cumming 2015-11-05 15:06:06 UTC
(In reply to Kjell Ahlstedt from comment #10)
> The easiest fix concerning trackable's move operators would be to remove
> "noexcept" from them. Is that acceptable?

Yes, sure.

In general, when we have a reason for something to be noexcept, or not, please leave a comment describing that reason. Then we won't have to decide again later.

Incidentally, is trackable::add_destroy_notify_callback() actually used or needed outside of libsigc++ and *mm. Maybe a future libsigc++ should try to hide that from the main public API.
Comment 13 Kjell Ahlstedt 2015-11-06 14:04:52 UTC
Created attachment 314991 [details] [review]
patch: trackable, slot, signal: Remove noexcept specifications

Now it remains to remove noexcept specifications and add missing move operators.
This patch removes noexcept. Can it be implemented in libsigc++ 2.6.x?
Or is it an API break that must wait for 2.7.x?
Comment 14 Kjell Ahlstedt 2015-11-06 14:06:14 UTC
Created attachment 314992 [details] [review]
patch: slot and signal: Add missing move constructors and move assignments

This patch adds missing move operators. I suppose it must wait for 2.7.x.

There are methods in libsigc++ that don't throw exceptions.
If noexcept specifications are important, I can find some of these methods and
add some noexcept specifications. Would such added specifications be an API break
that must wait for 2.7.x?
Comment 15 Kjell Ahlstedt 2015-11-07 09:48:01 UTC
(In reply to Murray Cumming from comment #11)
> Incidentally, is trackable::add_destroy_notify_callback() actually used or
> needed outside of libsigc++ and *mm. Maybe a future libsigc++ should try to
> hide that from the main public API.

Perhaps no one outside libsigc++ needs trackable::add_destroy_notify_callback().
But even if we hide it, that won't make trackable::notify_callbacks() noexcept.
The slot_rep::notify() callback calls the callback set by slot_base::set_parent()
and it calls the destructor of the stored functor, which can be anything.
A throwing destructor is a bad thing, but libsigc++ has no control of what such
a destructor might do.
Comment 16 Murray Cumming 2015-11-07 12:07:18 UTC
(In reply to Kjell Ahlstedt from comment #13)
> This patch removes noexcept. Can it be implemented in libsigc++ 2.6.x?

Yes, I think that should be fine. I don't think noexcept is part of the ABI. And this would not be a disruptive API change.
Comment 17 Murray Cumming 2015-11-07 12:12:36 UTC
(In reply to Kjell Ahlstedt from comment #14)
> Created attachment 314992 [details] [review] [review]
> patch: slot and signal: Add missing move constructors and move assignments
> 
> This patch adds missing move operators. I suppose it must wait for 2.7.x.

Please just go ahead with it now. I don't think enough people can already be using libsigc++ 2.6 for the ABI addition to be disruptive. I think it's worth it to get 2.6.x generally correct, to simplify things.

> There are methods in libsigc++ that don't throw exceptions.
> If noexcept specifications are important,

I tried to add them where possible for move operations, but that allows standard containers to use them. I think they can generally improve performance by removing the need for the compiler to add exception handling. It seems wise to use them where we can, though it's easy to get it wrong.

> I can find some of these methods
> and
> add some noexcept specifications. Would such added specifications be an API
> break
> that must wait for 2.7.x?

I think that would be fine. I don't think it would affect application developers.

Thanks as usual.
Comment 18 Murray Cumming 2015-11-07 12:13:12 UTC
> I tried to add them where possible for move operations, but that allows
> standard containers to use them.

I mean "because that allows", rather than "but that allows".
Comment 19 Kjell Ahlstedt 2015-11-08 09:26:18 UTC
I have pushed the two patches.

I'll add some "noexcept" later. That can be done without reference to a bug
report. I close this bug now.

> I tried to add them where possible for move operations, because that allows
> standard containers to use them.

Must move operations be declared noexcept for STL containers to use them?
The description of std::vector::push_back() at
http://en.cppreference.com/w/cpp/container/vector/push_back says

  If T's move constructor is not noexcept and T is not CopyInsertable into
  *this, vector will use the throwing move constructor. If it throws, the
  guarantee is waived and the effects are unspecified.

If this is typical of the STL containers, they can use all move operators, but
if a move operator does throw an exception, the program can crash in an
unspecified way.
Comment 20 Murray Cumming 2015-11-08 11:54:27 UTC
(In reply to Kjell Ahlstedt from comment #19)
> Must move operations be declared noexcept for STL containers to use them?
[snip]

Maybe something has changed in the standard? "Effective Modern C++" says "All these functions replace calls to copy operations in C++98 with calls to move operations in C++11 only if the move operations are known to not emit exceptions"
This link seems to get you near to that text in the book:
http://tinyurl.com/o9pgptp
Maybe it's wrong now.
Comment 21 Murray Cumming 2015-11-08 12:04:53 UTC
There's some earlier push_back documentation here, and a snippet of the standard:
http://stackoverflow.com/questions/11488490/vectorpush-back-insists-on-using-copy-constructor-though-a-move-constructor-is

I think a throwing move constructor (and hence "undefined" crashing) will only be used if the copy constructor is not available. So it really would be nice to have noexcept move operations where possible, so the standard containers can use them in normal cases. However, I understand that we can't do it in this case.
Comment 22 Kjell Ahlstedt 2015-11-08 18:11:32 UTC
> Maybe something has changed in the standard?

Maybe it has not. The text I linked to in comment 19 says that a move constructor
which is not noexcept is used _if_the_object_is_not_CopyInsertable_. I did not
pay enough attention to that clause at first. It's what your links also say.
They also explain why a possibly throwing copy constructor is preferred to a
possibly throwing move constructor.

This would mean that gtkmm widgets, which are not copyable, can be moved to
an STL container, such as a std::vector, whether or not their move constructors
are declared noexcept.
Comment 23 Murray Cumming 2015-11-12 09:08:42 UTC
I'll make a new tarball release soon, if you think that's OK.
Comment 24 Kjell Ahlstedt 2015-11-12 10:07:54 UTC
Created attachment 315313 [details] [review]
patch: Add a moving signal::connect() overload

I've been experimenting with a moving version of signal::connect(), and I was
just about to ask if it would fit in libsigc++ 2.6.2, or if it shall wait
until 2.7.x.


If there is a moving signal::connect() overload, i.e. one that takes an
rvalue reference of a slot, then slot's move constructor is used in many
situations where its copy constructor is now used.

I have added moving signal#::connect() and moving overloads of some other
connect() and insert() methods, and run some tests.

The following test results show the number of calls to slot_base's move
operators when libsigc++'s test suite is executed. Copy/move insert() shows
the number of calls to signal_impl::insert(), which is called from
signal#::connect().

Without moving versions of signal::connect() and friends

Copy insert()                33
Move insert()                 0  does not exist
Copy constructor             45
Copy assignment operator      2
Move constructor             31
  of which really moving         1
Move assignment operator     24
  of which really moving        22

With moving versions of signal::connect() and friends

Copy insert()                 0
Move insert()                33
Copy constructor             10
Copy assignment operator      2
Move constructor             64
  of which really moving        34
Move assignment operator     24
  of which really moving        22

"Really moving" means that a call to a move operator has actually moved a
slot_rep instance from one slot to another. Some calls to a move operator does
not really move, in most cases because the slot is empty, and there is nothing
to move. Each time a signal is emitted, an empty slot is inserted at the end of
the slot list, and removed after the emission.

The test suite is hardly an example of typical use of libsigc++, but these
test results still show that it's worthwhile to add moving connect() and
insert() methods. I'm sure it would be even more worthwhile to do something
similar in glibmm.

Why is slot's move constructor used although it may throw exceptions and
there is a corresponding copy constructor? We have just learned that STL avoids
such move constructors. Or haven't we? I don't think we have. We have learned
that std::vector avoids such move constructors. But a signal stores slots in a
std::list, and a list does not have the same reason to avoid exceptions as a
vector has. When an element is added to or removed from a list, no other
element is reallocated. That is in fact an essential characteristic of a list,
which is (I suppose) the reason why it was chosen as the container of connected
slots in a sigc::signal. Much of the code in sigc::signal and sigc::connection
would not work if connected slots were ever reallocated.
Comment 25 Murray Cumming 2015-11-12 10:29:25 UTC
Great. It sounds worthwhile.

But maybe we should delay this one until 2.7, just to reduce some risk?
Comment 26 Kjell Ahlstedt 2015-11-12 15:07:57 UTC
An obvious risk is this: It's new API and ABI. If it's included in 2.6.2, a
program built with 2.6.2 may not be able to run with an object library
file of version 2.6.1. or 2.6.0.

I don't think the same risk exists with the fixes in sigc::slot and sigc::signal,
because the added move operators are defined in template classes in header files.

Suggested way forward:
You release libsigc++ 2.6.2 whenever you like.
When that's done, I can make a libsigc-2-6 branch and add the new
signal::connect() method to the master branch. I'll just change the @newin
directives in my patch from 2.6.2 to 2.8.
Comment 27 Kjell Ahlstedt 2015-11-16 14:38:07 UTC
I have pushed a patch that adds a moving signal::connect() overload. It's
identical to the patch in comment 24 except for the @newin directives.
https://git.gnome.org/browse/libsigc++2/commit/?id=31132598511b2bfaa6671acbf6c2ed5a5eb151c6