Giter Site home page Giter Site logo

nod's Introduction

Nod

Build Status GitHub tag

Dependency free, header only signals and slot library implemented with C++11.

Usage

Simple usage

The following example creates a signal and then connects a lambda as a slot.

// Create a signal which accepts slots with no arguments and void return value.
nod::signal<void()> signal;
// Connect a lambda slot that writes "Hello, World!" to stdout
signal.connect([](){
		std::cout << "Hello, World!" << std::endl;
	});
// Call the slots
signal();

Connecting multiple slots

If multiple slots are connected to the same signal, all of the slots will be called when the signal is invoked. The slots will be called in the same order as they where connected.

void endline() {
	std::cout << std::endl;
}

// Create a signal
nod::signal<void()> signal;
// Connect a lambda that prints a message
signal.connect([](){
		std::cout << "Message without endline!";
	});
// Connect a function that prints a endline
signal.connect(endline);

// Call the slots
signal();

Slot type

The signal types in the library support connection of the same types that is supported by std::function<T>.

Slot arguments

When a signal calls it's connected slots, any arguments passed to the signal are propagated to the slots. To make this work, we do need to specify the signature of the signal to accept the arguments.

void print_sum( int x, int y ) {
	std::cout << x << "+" << y << "=" << (x+y) << std::endl;
}
void print_product( int x, int y ) {
	std::cout << x << "*" << y << "=" << (x*y) << std::endl;
}


// We create a signal with two integer arguments.
nod::signal<void(int,int)> signal;
// Let's connect our slot
signal.connect( print_sum );
signal.connect( print_product );

// Call the slots
signal(10, 15);
signal(-5, 7);	

Disconnecting slots

There are many circumstances where the programmer needs to diconnect a slot that no longer want to recieve events from the signal. This can be really important if the lifetime of the slots are shorter than the lifetime of the signal. That could cause the signal to call slots that have been destroyed but not disconnected, leading to undefined behaviour and probably segmentation faults.

When a slot is connected, the return value from the connect method returns an instance of the class nod::connection, that can be used to disconnect that slot.

// Let's create a signal
nod::signal<void()> signal;
// Connect a slot, and save the connection
nod::connection connection = signal.connect([](){
								 std::cout << "I'm connected!" << std::endl;
							 });
// Triggering the signal will call the slot
signal();
// Now we disconnect the slot
connection.disconnect();
// Triggering the signal will no longer call the slot
signal();

Scoped connections

To assist in disconnecting slots, one can use the class nod::scoped_connection to capture a slot connection. A scoped connection will automatically disconnect the slot when the connection object goes out of scope.

// We create a signal
nod::signal<void()> signal;
// Let's use a scope to control lifetime
{ 
	// Let's save the connection in a scoped_connection
	nod::scoped_connection connection =
		signal.connect([](){
			std::cout << "This message should only be emitted once!" << std::endl; 
		});
	// If we trigger the signal, the slot will be called
	signal();
} // Our scoped connection is destructed, and disconnects the slot
// Triggering the signal now will not call the slot
signal();	

Slot return values

Accumulation of return values

It is possible for slots to have a return value. The return values can be returned from the signal using a accumulator, which is a function object that acts as a proxy object that processes the slot return values. When triggering a signal through a accumulator, the accumulator gets called for each slot return value, does the desired accumulation and then return the result to the code triggering the signal. The accumulator is designed to work in a similar way as the STL numerical algorithm std::accumulate.

// We create a singal with slots that return a value
nod::signal<int(int, int)> signal;
// Then we connect some signals
signal.connect( std::plus<int>{} );
signal.connect( std::multiplies<int>{} );
signal.connect( std::minus<int>{} );		
// Let's say we want to calculate the sum of all the slot return values
// when triggering the singal with the parameters 10 and 100.
// We do this by accumulating the return values with the initial value 0
// and a plus function object, like so:
std::cout << "Sum: " << signal.accumulate(0, std::plus<int>{})(10,100) << std::endl;
// Or accumulate by multiplying (this needs 1 as initial value):
std::cout << "Product: " << signal.accumulate(1, std::multiplies<int>{})(10,100) << std::endl;
// If we instead want to build a vector with all the return values
// we can accumulate them this way (start with a empty vector and add each value):			
auto vec = signal.accumulate( std::vector<int>{}, []( std::vector<int> result, int value ) {
		result.push_back( value );
		return result;
	})(10,100);

std::cout << "Vector: ";
for( auto const& element : vec ) {
	std::cout << element << " "; 
}
std::cout << std::endl;

Aggregation

As we can see from the previous example, we can use the accumulate method if we want to aggregate all the return values of the slots. Doing the aggregation that way is not very optimal. It is both a inefficient algorithm for doing aggreagtion to a container, and it obscures the call site as the caller needs to express the aggregation using the verb accumulate. To remedy these shortcomings we can turn to the method aggregate instead. This is a template method, taking the type of container to aggregate to as a template parameter.

// We create a singal
nod::signal<int(int, int)> signal;
// Let's connect some slots
signal.connect( std::plus<int>{} );
signal.connect( std::multiplies<int>{} );
signal.connect( std::minus<int>{} );
// We can now trigger the signal and aggregate the slot return values
auto vec = signal.aggregate<std::vector<int>>(10,100);

std::cout << "Result: ";
for( auto const& element : vec ) {
	std::cout << element << " "; 
}
std::cout << std::endl;

Thread safety

There are two types of signals in the library. The first is nod::signal<T> which is safe to use in a multi threaded environment. Multiple threads can read, write, connect slots and disconnect slots simultaneously, and the signal will provide the nessesary synchronization. When triggering a slignal, all the registered slots will be called and executed by the thread that triggered the signal.

The second type of signal is nod::unsafe_signal<T> which is not safe to use in a multi threaded environment. No syncronization will be performed on the internal state of the signal. Instances of the signal should theoretically be safe to read from multiple thread simultaneously, as long as no thread is writing to the same object at the same time. There can be a performance gain involved in using the unsafe version of a signal, since no syncronization primitives will be used.

nod::connection and nod::scoped_connection are thread safe for reading from multiple threads, as long as no thread is writing to the same object. Writing in this context means calling any non const member function, including destructing the object. If an object is being written by one thread, then all reads and writes to that object from the same or other threads needs to be prevented. This basically means that a connection is only allowed to be disconnected from one thread, and you should not check connection status or reassign the connection while it is being disconnected.

Building the tests

The test project uses premake5 to generate make files or similiar.

Linux

To build and run the tests using gcc and gmake on linux, execute the following from the test directory:

premake5 gmake
make -C build/gmake
bin/gmake/debug/nod_tests

Visual Studio 2013

To build and run the tests, execute the following from the test directory:

REM Adjust paths to suite your environment
c:\path\to\premake\premake5.exe vs2013
"c:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\Tools\vsvars32.bat"
msbuild /m build\vs2013\nod_tests.sln
bin\vs2013\debug\nod_tests.exe

The MIT License (MIT)

Copyright (c) 2015 Fredrik Berggren

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

nod's People

Contributors

fr00b0 avatar jeppefrandsen avatar kevin-- avatar oliverdaniell avatar simonweinhold avatar timblechmann avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

nod's Issues

disconnect slot using function instead of connection class

Hi,

I'm trying to find a way to disconnect a slot without having to store its connection class.
I tried many different things but never could write compilable code.

What I'm trying to do is the following:

// connection, as usual (in the case the slot has one parameter), but without storing the connection class
signal.connect(std::bind(&Foo::bar, this, std::placeholders::_1));

// disconnection, without using the connection class
// could be like this
signal.connect(std::bind(&Foo::bar, this, std::placeholders::_1));
// or like this (I won't have several connections from a single signal to multiple slots within my object)
signal.connect(this);

Is this achievable somehow ?

Slots copy is unnecessary for non parallel use

Hello,

I noticed you are copying the slots when emit is called :

for( auto const& slot : copy_slots() ) {
	if( slot ) {
		value = func( value, slot( args... ) );
	}
}

I understand why it is necessary in a parallel context, but it's also happening with unsafe_signal, slowing drastically the call to this function for no benefits.

Detailed usage example

** Hello,

I lack the knowledge for the "signal.connect" syntax.
It would be appreciated if somebody corrects my code which is compiling and running on VS2015
except the marked part (#if0 ... #endif).
Or show me a implementation where the "slots" are executing in there associated threads.

Best Regards,
Antal
**

// SigSlotX.cpp : Test example code

//#include "stdafx.h"
#include <string>         // std::string
#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <chrono>         // std::chrono::seconds
#include <windows.h>      // GetCurrentThreadId()
#include "nod.hpp"       


class MyClassA {
public:
    /* Explicitly using the default constructor to
    * underline the fact that it does get called */
    MyClassA() : the_thread() {}
    ~MyClassA() {
        stop_thread = true;
        if (the_thread.joinable()) the_thread.join();
    }
    void Start() {
        // This will start the thread. Notice move semantics!
        the_thread = std::thread(&MyClassA::ThreadMain, this);
    }

    // Function that prints the sum calculation of two integers
    void print_sumT(int x, int y) {
        std::cout << "\n   print_sum =>   " << x << "+" << y << "=" << (x + y);
        std::cout << "\t of thread - " << GetCurrentThreadId() << std::endl;
    }
    // Function that prints the string
    void print_string(std::string st) {
        std::cout << "\n   print_string =>   " <<  st ;
        std::cout << "\t of thread - " << GetCurrentThreadId() << std::endl;
    }


private:
    std::thread the_thread;
    /****/
    bool stop_thread = false; // Super simple thread stopping.
    void ThreadMain() 
    {
        while (!stop_thread) 
        {   // Do something useful, e.g:
            std::this_thread::sleep_for(std::chrono::seconds(1));
            std::cout << "\n   *   ";
            std::cout << "\t of thread - " << GetCurrentThreadId() << std::endl;
         }
       }
};

int main()
{
    std::string theSting1 = "Hello I am the sting A ";
    std::string theSting2 = "Hello I am the sting B ";

     MyClassA * pA = new MyClassA;
     MyClassA  aA;
     pA->Start();
     aA.Start();

    // We create a signal with two integer arguments.  MyClassA
    nod::signal<void(int, int)> signalA;
    nod::signal<void(std::string)> signalB;
    // Let's connect our slot

#if 0 
        // The non compiling part:  What is the correct implementation?
       signalA.connect(pA->print_sumT);  
       signalA.connect(aA.print_sumT);
       signalB.connect(pA->print_sumT);
       signalB.connect(aA.print_sumT);
#endif

    // Call the slots,  I would like that these are executed in the object's thread:
    signalA(10, 15);
    signalA(-5, 7);
    signalB(theSting1);
    signalB(theSting2);

    std::cout << "\n <> main, ant the treads now execute concurrently..." ;
    std::cout << "\t of thread - " << GetCurrentThreadId() << std::endl;

     // These are executed in main thread: 
    pA->print_string( theSting1);
    aA.print_string( theSting2);
    pA->print_sumT(22, 44);
    aA.print_sumT(3, 77);

    std::this_thread::sleep_for(std::chrono::seconds(5));

    std::cout << "\n   Thread tests are completed.";
    std::cout << "\t of thread - " << GetCurrentThreadId() << std::endl;
    return 0;
}

(This should be one continuous code.)

Conan package

I made one here: https://github.com/Lawrencemm/conan_nod

I might try to get it added to the bincrafters or conan community repositories.

If you wanted I guess you could upload this to your own remote and add a link to the remote in the readme for getting nod easily.

Attempting to reference a deleted function

Hi, in your readme you have several locations where you do:

connection = signal.connect ...

This operator is deleted and only a move constructor remains. This causes errors in VS about deleted functions. Eventually found the actual expected usage of the nod connection objects in your unit tests. I would recommend correcting the readme as this causes some confusion.

Add contributor list

Who has contributed to the library should be visible either in the README or in the nod.hpp header, or both.

Signals created via move constructor/assignment use the moved from object

Move support for signal_type added in #19 has a major flaw - the signal created by a move constructor/assignment keeps a pointer (_shared_disconnector) to _disconnector stored inside the moved from object.

If the original object is then deleted, any operation that uses the disconnector (another move, connection::disconnect) will try to access a freed memory and will most likely crash.
This can be easily detected by AddressSanitizer using a code like this:

#include "nod.hpp"
#include <memory>
int main()
{
	auto sig1 = std::make_unique<nod::signal<void ()>>();
	auto conn = sig1->connect([]{}); // initializes `_shared_disconnector`
	auto sig2 = std::move(*sig1);
	sig1.reset();
	// Any of the following lines tries to access memory of the deleted *sig1 object:
	conn.disconnect();
	auto sig3 = std::move(sig2);
}

Compiling (g++ -g -O1 -fsanitize=address nod_move_fail.cpp) and running this code with ASan results in this report:

==12348==ERROR: AddressSanitizer: heap-use-after-free on address 0x60b000000138 at pc 0x557ffecfb967 bp 0x7fff01c98960 sp 0x7fff01c98950
READ of size 8 at 0x60b000000138 thread T0                                                                                                                                                                                                                                                
    #0 0x557ffecfb966 in nod::connection::disconnect() /tmp/nod.hpp:657
    #1 0x557ffecfa32c in main /tmp/nod_move_fail.cpp:10
    #2 0x7f0b36963e9d in __libc_start_main (/lib64/libc.so.6+0x23e9d)
    #3 0x557ffecf9269 in _start (/tmp/a.out+0x2269)

0x60b000000138 is located 72 bytes inside of 104-byte region [0x60b0000000f0,0x60b000000158)
freed by thread T0 here:                                                                                                                                                                                                                                                                  
    #0 0x7f0b36f1ba87 in operator delete(void*, unsigned long) (/usr/lib/gcc/x86_64-pc-linux-gnu/11.1.0/libasan.so.6+0xb3a87)
    #1 0x557ffecfa320 in std::default_delete<nod::signal_type<nod::multithread_policy, void ()> >::operator()(nod::signal_type<nod::multithread_policy, void ()>*) const /usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/include/g++-v10/bits/unique_ptr.h:85
    #2 0x557ffecfa320 in std::__uniq_ptr_impl<nod::signal_type<nod::multithread_policy, void ()>, std::default_delete<nod::signal_type<nod::multithread_policy, void ()> > >::reset(nod::signal_type<nod::multithread_policy, void ()>*) /usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/include/g++-v10/bits/unique_ptr.h:182
    #3 0x557ffecfa320 in std::unique_ptr<nod::signal_type<nod::multithread_policy, void ()>, std::default_delete<nod::signal_type<nod::multithread_policy, void ()> > >::reset(nod::signal_type<nod::multithread_policy, void ()>*) /usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/include/g++-v10/bits/unique_ptr.h:456
    #4 0x557ffecfa320 in main /tmp/nod_move_fail.cpp:8

previously allocated by thread T0 here:
    #0 0x7f0b36f1aa97 in operator new(unsigned long) (/usr/lib/gcc/x86_64-pc-linux-gnu/11.1.0/libasan.so.6+0xb2a97)
    #1 0x557ffecf94b4 in std::_MakeUniq<nod::signal_type<nod::multithread_policy, void ()> >::__single_object std::make_unique<nod::signal_type<nod::multithread_policy, void ()>>() /usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/include/g++-v10/bits/unique_ptr.h:962
    #2 0x557ffecf94b4 in main /tmp/nod_move_fail.cpp:5

SUMMARY: AddressSanitizer: heap-use-after-free /tmp/nod.hpp:657 in nod::connection::disconnect()
Shadow bytes around the buggy address:
  0x0c167fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c167fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c167fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c167fff8000: fa fa fa fa fa fa fa fa fd fd fd fd fd fd fd fd
  0x0c167fff8010: fd fd fd fd fd fa fa fa fa fa fa fa fa fa fd fd
=>0x0c167fff8020: fd fd fd fd fd fd fd[fd]fd fd fd fa fa fa fa fa
  0x0c167fff8030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c167fff8040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c167fff8050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c167fff8060: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c167fff8070: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
  Shadow gap:              cc

I don't know how this can be fixed without creating the disconnector on the heap.

Add signal type that only accepts one slot

If a signal only has a single slot connected, we could allow that arguments are moved into the operator() and then forwarded into the slot. This might open up some interesting use-cases, where the signal consumes the argument by taking ownership. Since we can't have several slots taking ownership of the same thing, we would need a new type of signal that only allows a single slot to be connected.

This issue was spawned from #4

potential data race in emission

Hello, recently I have been working on the next release of nano-signal-slot for some time now and have encountered a data race issue with my current iteration. Curious how others solved it I started looking into other thread safe implementations. Perusing nod I noticed the exact same signature of the issue exists in nod.

The signature is:

for (auto slot : copied_slots)
{
    if (slot)
        slot(args...)
}

The issue is the lock for emission is released as soon as the copied_slots is created. Due to this there is now a "potential data race" where the slot target "could" be destructed before being called in the emission loop.

Edit: Also my first attempt at resolving this is the same as how you currently test your shared disconnector in destruction. However, this would be UB now instead of a data race as accessing a class in destruction is UB.

Slot return values

Design and implement some way of capturing slot return values when triggering signals.

signal_type add a rvalue operator

hi, the signal_type's opertator is just surpport lvalue, it need a rvalue version, like this.

void operator()(A&&... args) const {
    for (auto const& slot : copy_slots()) {
        if (slot) {
                  slot(std::forward<A>(args)...);
            }
    }
}

Issue with rvalue/lvalue binding using accumulate

As reported by @oliverdaniell in pull request #14, there is an rvalue/lvalue issue with accumulate (actually the accumulator proxy object)

The issue can be triggered with the following code, and results in a compiler error:

nod::signal<int(std::shared_ptr<std::string>)> signal;
auto accumulator = signal.accumulate(0, std::plus<int>{});
auto ptr = std::make_shared<std::string>("test");
accumulator(ptr);

When compiling this using Visual Studio, we get the following errors, even though the code should be valid:

error C2664: 'int nod::signal_accumulator<nod::signal_type<nod::multithread_policy,int (std::shared_ptr<std::string>)>,int,std::plus<int>,std::shared_ptr<std::string>>::operator ()(std::shared_ptr<std::string> &&) const': cannot convert argument 1 from 'std::shared_ptr<std::string>' to 'std::shared_ptr<std::string> &&'
note: You cannot bind an lvalue to an rvalue reference

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.