Skip to content

Allow slots with rvalue reference parameters #74

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

slavaandrejev
Copy link
Contributor

@slavaandrejev slavaandrejev commented May 26, 2021

In code based on Glibmm it's quite common to convert a Glib C structure
to a Glib::Object's descendant before passing it as a parameter to a
slot. If the slot is not supposed to modify the object, then everything
is fine, we can declare the parameter as a const reference and the
temporary object will be accurately dealt with by the compiler. However,
if the Glib based object is getting modified, the only way to pass it to
the slot is to declare it a Glib::RefPtr shared pointer. This creates a
lot of unnecessary churn of memory allocations for a simple deal of
calling a signal. However, C++ has a specific mean for this particular
semantic of passing a temporary object: rvalue reference.

For example, somewhere inside Gtkmm might be a declaration:

_WRAP_SIGNAL(void sun_rose(const Glib::RefPtr<Gtk::Sun> &new_sun), "sun-rose")

Instead its more semantically correct to write:

_WRAP_SIGNAL(void sun_rose(Gtk::Sun &&new_sun), "sun-rose")

And later somewhere in your code:

world->signal_sun_rose().connect([&](auto &&new_sun) {
  new_sun.shine();
});

This commit makes a couple of simple modifications that allow declaring
signals and slots with rvalue reference parameter.

In code based on Glibmm it's quite common to convert a Glib C structure
to a Glib::Object's descendant before passing it as a parameter to a
slot. If the slot is not supposed to modify the object, then everything
is fine, we can declare the parameter as a const reference and the
temporary object will be accurately dealt with by the compiler. However,
if the Glib based object is getting modified, the only way to pass it to
the slot is to declare it a Glib::RefPtr shared pointer. This creates a
lot of unnecessary churn of memory allocations for a simple deal of
calling a signal. However, C++ has a specific mean for this particular
semantic of passing a temporary object: rvalue reference.

For example, somewhere inside Gtkmm might be a declaration:
_WRAP_SIGNAL(void sun_rose(const Glib::RefPtr<Gtk::Sun> &new_sun),
"sun-rose")

Instead its more semantically correct to write:
_WRAP_SIGNAL(void sun_rose(Gtk::Sun &&new_sun), "sun-rose")

And later somewhere in your code:
world->signal_sun_rose().connect([&](auto &&new_sun) {
  new_sun.shine();
});

This commit makes a couple of simple modifications that allow declaring
signals and slots with rvalue reference parameter.
@kjellahl
Copy link
Contributor

I expanded your test case to see what happens if you connect more than one signal
handler. I also simulated that the signal handlers actually use their right to
move from the object to which they get an rvalue reference. As I suspected, the
second signal handler tries to use an object which the first signal handler has
moved from. I've attached my extended version of your test case. The output is

test_signal()
Default ctor
Move ctor
Dtor m_object_has_been_moved_from=0
Move ctor
*** Attempt to use moved-from object ***
Dtor m_object_has_been_moved_from=0
Dtor m_object_has_been_moved_from=1

test_slot()
Default ctor
Move ctor
Dtor m_object_has_been_moved_from=0
Dtor m_object_has_been_moved_from=1

This behavior is too risky. We would have to describe unnatural restrictions on
the signals and/or signal handlers (slots). For instance that only one signal handler
is allowed. Or that the signal handlers are not allowed to move from an object
to which they get an rvalue reference.

Concerning signals in glibmm and gtkmm, I don't quite understand what temporary
objects you want to be able to move from instead of copy from. In test_rvalue_ref.cc
you specify by the call signal(std::move(x)) that x can be treated as a temporary
object. In glibmm and gtkmm signals are almost always emitted differently.
Signal handlers in C++ code are called, when C code in glib or gtk calls g_signal_emit().

An aside: My comment in the gtkmm-documentation issue https://gitlab.gnome.org/GNOME/gtkmm-documentation/-/issues/10
explains why it's not always preferable to use lambda expressions instead of sigc::mem_fun().

test_rvalue_ref.zip

@slavaandrejev
Copy link
Contributor Author

slavaandrejev commented May 30, 2021

Thank you for spending your time investigating the proposed change. You raised a great question. Let me address it.

The situation you described is not particular to rvalue references. It's not even particular to C++. It can happen with anything that is being modified by a signal handler. Here is an example from the official GStreamer tutorial:

g_signal_connect (data.source, "pad-added", G_CALLBACK (pad_added_handler), &data);
// ...
void pad_added_handler (GstElement *src, GstPad *new_pad, CustomData *data) {
// ...
    ret = gst_pad_link (new_pad, sink_pad);
}

If you have n handlers they will try to connect a poor pad n times. With the current sigc++ the correspondent C++ signal declaration will look like this:

_WRAP_SIGNAL(void pad_added(const Glib::RefPtr<Gst::Pad>& new_pad), "pad-added")

_WRAP_SIGNAL will generate an implementation that creates a new Glib::RefPtr and will pass it by const reference, because this is what you must do to bind an rvalue to a parameter according to pre C++11.

At this point nothing prevents you to do whatever you want with Gst::Pad inside the signal handler, including stealing its guts, i.e. nothing helps you avoiding the same trouble you described in your test case when more than one handler modify the object.

There is more to it. The suggested change is intended for a case when you do modify the underlying object. If you do it more than once, you are doomed regardless of whether it's an rvalue reference or not, and nothing prevents you from doing this in sigc++. Giving that if you look at the declaration:

_WRAP_SIGNAL(void pad_added(const Glib::RefPtr<Gst::Pad>& new_pad), "pad-added")

you have no knowledge if you can modify Gst::Pad in more than one handler or not. This is exactly the case where the additional documentation is needed to describe "unnatural restrictions". As oppose when you see

_WRAP_SIGNAL(void pad_added(Gst::Pad &&new_pad), "pad-added")

It begs you with explicitely expressed semantic to pay attention to what you are doing. Isn't it more clear now that you probably shouldn't mess up with the guts of new_pad in more than one handler? In the case of const Glib::RefPtr<Gst::Pad>& new_pad it is absolutely obscure.

The bottom line is: it doesn't add more doom, on opposite it expresses the clear intent and begs the programmer to pay attention.

Concerning signals in glibmm and gtkmm, I don't quite understand what temporary objects you want to be able to move from instead of copy from.

Rvalue reference is just a hint from the compiler that the object is temporary, "move" is an agreement that exists only for constructors and assignemnt operators. In contrast for regular methods rvalue reference doesn't mean you have to do a move or anything else. When a C object arrives to a C++ handler it must be converted to a C++ object. This C++ wrapper is temporary, just to pass the parameter. Creating a second wrapper (shared_ptr) around the first wrapper just to pass a parameter is absolutly unnecessary, because the lifetime of a callee is a strict subset of the caller. In addition it allocates memory on heap for strong and weak reference counters. Every time, just to pass a parameter, you allocate 8 bytes (4 for ref count and 4 for weak ref count) on heap, and then immediately delete it. Besides performace concerns it also creates holes in the heap, eventually it starts reminding Swiss cheese, the amount of consumed memory grows, though it's not allocated.

@kjellahl
Copy link
Contributor

kjellahl commented Jun 1, 2021

Bjarne Stroustrup describes the different types of references like so
(The C++ Programming Language, 4th edition, page 190).

  • lvalue references: to refer to objects whose value we want to change
  • const references: to refer to objects whose value we do not want to change (e.g. a constant)
  • rvalue references: to refer to objects whose value we do not need to preserve after we have used it (e.g. a temporary)

I don't understand what would be wrong with a non-const lvalue reference in the example
with signal_pad_added() that you mention. To me, it seems more appropriate than an
rvalue reference, if you think reference counting with std::shared_ptr is too costly.

This C++ wrapper is temporary

If the C object is a descendant of GObject, the C++ wrapper is not temporary.
The first time a wrapper is needed, it's created and its address is saved in the
GObject instance with g_object_set_qdata_full(). The next time a C++ wrapper is
needed, its address is fetched with g_object_get_qdata(). Thus, if signal handlers
are called several times with the same GObject instance as a parameter, the same
C++ wrapper will be used each time.

@slavaandrejev
Copy link
Contributor Author

slavaandrejev commented Jun 1, 2021

Why are you citing Stroustrup to me? I am reading his C++ books since 90s.

I don't understand what would be wrong with a non-const lvalue reference in the example

You can't pass Gst::Pad to it, if Gst::Pad is an rvalue. Only const lvalue references can be bound to rvalues.

If the C object is a descendant of GObject, the C++ wrapper is not temporary.

You can pass only descendants of GObject as a parameter?

Why are we discussing the qualities of the example not related to the qualities of the proposed change? You had a concern about multiple handlers modifying the same object. I showed you that this is not specific to rvalue references. Honestly, I think that somebody just forgot that univeral references must be forwarded in a couple of places when converted the code from .m4 macros. Let's make a fix that can benefit the users.

@kjellahl kjellahl merged commit 7d85510 into libsigcplusplus:master Jun 2, 2021
@slavaandrejev
Copy link
Contributor Author

slavaandrejev commented Jun 2, 2021

Thank you for merging and for a good discussion!

@kjellahl
Copy link
Contributor

kjellahl commented Jun 3, 2021

I think that somebody just forgot that univeral references must be forwarded in a couple of places when converted the code from .m4 macros.

I tried to compile your test case in libsigc++2 (the libsigc++-2-10 branch),
which contains .m4 macros. It failed. The first few error messages:

../../gnome/libsigcplusplus-2.0/tests/test_rvalue_ref.cc:28:22: error: no match for call to ‘(sigc::signal<void(MoveableStruct&&)>) (std::remove_reference<MoveableStruct&>::type)’
   28 |   signal(std::move(x));
      |                      ^
In file included from ../../gnome/libsigcplusplus-2.0/tests/test_rvalue_ref.cc:3:
./sigc++/signal.h:2966:15: note: candidate: ‘sigc::signal1<T_return, T_arg1, T_accumulator>::result_type sigc::signal1<T_return, T_arg1, T_accumulator>::operator()(sigc::type_trait_take_t<T_arg2>) const [with T_return = void; T_arg1 = MoveableStruct&&; T_accumulator = sigc::nil; sigc::signal1<T_return, T_arg1, T_accumulator>::result_type = void; sigc::type_trait_take_t<T_arg2> = MoveableStruct&]’ (near match)
 2966 |   result_type operator()(type_trait_take_t<T_arg1> _A_a1) const
      |               ^~~~~~~~
./sigc++/signal.h:2966:15: note:   conversion of argument 1 would be ill-formed:
../../gnome/libsigcplusplus-2.0/tests/test_rvalue_ref.cc:28:19: error: cannot bind non-const lvalue reference of type ‘sigc::type_trait_take_t<MoveableStruct&&>’ {aka ‘MoveableStruct&’} to an rvalue of type ‘std::remove_reference<MoveableStruct&>::type’ {aka ‘MoveableStruct’}
   28 |   signal(std::move(x));
      |          ~~~~~~~~~^~~

I think that slots with rvalue references have never worked. Have you seen it
working with any version of libsigc++?

I'm slowly (too slowly?) beginning to understand why you want to use rvalue refs.
With a suitable _CONVERSION() macro you can change

_WRAP_SIGNAL(void pad_added(const Glib::RefPtr<Gst::Pad>& new_pad), "pad-added")

to

_WRAP_SIGNAL(void pad_added(Gst::Pad&& new_pad), "pad-added")

If the intent is to make it possible for the signal handler to call non-const methods
in new_pad, but not to move from it, I still think it would be more appropriate with

_WRAP_SIGNAL(void pad_added(Gst::Pad& new_pad), "pad-added")

The signal handler need not know that the C++ wrapper (but not the wrapped GstPad, I suppose)
is a temporary object. But with a non-const lvalue ref the C++ wrapper can't be a temporary object.
You would have to write a hand-coded Element_signal_pad_added_callback() in gstreamermm's
element.ccg file, containing something like

Gst::Pad pad(c_instance);
slot(pad);

@slavaandrejev
Copy link
Contributor Author

slavaandrejev commented Jun 3, 2021

Why it would have worked? The code was introduced before C++11. Then it was refactored with occasional std::forward, but not in all places. It couldn't work before my change.

The signal handler need not know that the C++ wrapper (but not the wrapped GstPad, I suppose) is a temporary object.

Semantically it's temporary, because the signal is coming from the C code, thus the C++ wrapper is just to pass the parameter to the C++ code. That's why it is called a "wrapper". Caching it in the guts of GObject is an implementation detail. Therefore, in the proper design it should be the other way around: the handler need not know the C++ wrapper is NOT temporary.

You would have to write a hand-coded Element_signal_pad_added_callback()

No, I wouldn't. Everything works fine, exactly as I want without hand-written pieces.

@slavaandrejev slavaandrejev deleted the rvalue_ref_parameters branch December 20, 2021 06:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants