Forum Rule: Always post complete source code & details to reproduce any issue!
Page 1 of 2 1 2 LastLast
Results 1 to 25 of 36

Thread: Pointers... again

  1. #1

    Pointers... again

    I am stuck again. Take a look at the code below and please tell me why I can't get the testClick function to print "6". I have spent 2 days trying to work this out, looking at various resources online. I just can't work it out.

    I have simplified a class I am writing and a class I am extending in the code below. I think the problem I am having is with the referencing / de-referencing of pointers.

    Code:
    void testClick(void *id) {
      Serial.println( (int) id, DEC);
      Serial.println( (int) &id, DEC);
    }
    
    void setup() {
      Serial.begin(9600);
      Serial.println("start");
    
      int *buttonIdPtr;
      int buttonId = 6;
    
      buttonIdPtr = &buttonId;
      
      testClick(buttonIdPtr);
    }
    
    void loop() {
    
    }
    What I get is:

    Code:
    start
    537362412
    537362388
    Which looks like memory addresses, I expect (want) to see the integer "6" printed to serial.

    Background, I am extending the OneButton library (https://github.com/mathertel/OneButton) to suite my purposes of debouncing I/O expander and I'm trying to get my callback functions working (these will eventually generate CANbus messages from debounced button pushes on the I/O expander. The callback function must fit the typedef in the base OneButton library:

    Code:
    typedef void (*parameterizedCallbackFunction)(void*);
    hence my class method which takes a void* as a parameter. I need a way to get to the value of the void pointer passed in.
    Last edited by ilium007; 04-26-2020 at 12:26 PM.

  2. #2
    Junior Member
    Join Date
    Feb 2017
    Location
    Chicago, IL
    Posts
    16
    It is printing the memory address, because you are assigning the memory address of buttonId as its value with buttonIdPtr = &buttonId; Try buttonIdPtr = buttonId instead, that works in your code. Alternatively, leave the assignment of the address via &buttonId and change testClick to this:

    Code:
    void testClick(int *id) {  
      Serial.println( (int) *id, DEC);
    }

  3. #3
    Quote Originally Posted by beermat View Post
    It is printing the memory address, because you are assigning the memory address of buttonId as its value with buttonIdPtr = &buttonId; Try buttonIdPtr = buttonId instead, that works in your code. Alternatively, leave the assignment of the address via &buttonId and change testClick to this:

    Code:
    void testClick(int *id) {  
      Serial.println( (int) *id, DEC);
    }
    I will give it a go, but the problem is that the underlying OneButton class requires a void * as defined in its typedef for the callback function that gets passed in:

    Code:
    typedef void (*parameterizedCallbackFunction)(void*);

  4. #4
    Senior Member+ KurtE's Avatar
    Join Date
    Jan 2014
    Posts
    6,892
    Try something like:
    Code:
    void testClick(void *id) {
      Serial.println( *((int*) id), DEC);
    }

  5. #5
    Quote Originally Posted by KurtE View Post
    Try something like:
    Code:
    void testClick(void *id) {
      Serial.println( *((int*) id), DEC);
    }
    Name:  Screen Shot 2020-04-26 at 10.47.28 pm.png
Views: 189
Size:  11.4 KB

    I wish I was smart

    Ok, so that works..... can you please explain in words suitable for my intellect the pointer magic going on here?

    Thanks

  6. #6
    Senior Member+ KurtE's Avatar
    Join Date
    Jan 2014
    Posts
    6,892
    With the above: *((int*) id)
    It says cast (int*) the variable id to be a pointer to a type of int. From there get the contents of that variable * ...

    Yes I know cryptic...
    More long hand version:

    Code:
    void testClick(void *id) {
      int *id_int = (int*)id;
    
      Serial.println( *id_int, DEC);
    }
    Or sorry in advance maybe more confusing.
    Code:
    void testClick(void *id) {
      int *id_int = (int*)id;
    
      Serial.println(id_int[0], DEC);
    }
    Which says take the first element (0) from that pointer as if it was an array.

    Again why I mention this is often you will see places where maybe the calling code does something like:

    Code:
    int my_array[5] = {0, 1, 2, 3, 4};
    test_click(my_array);
    Note this could give compiler warning, so I would normally cast this to void*: test_click((void*)my_array);

    Sorry if that added confusion, but often one of the next thing that comes up with pointers, is passing arrays.

  7. #7
    Thanks so much. Makes a lot more sense now. I need to do a lot more reading.

  8. #8
    Senior Member PaulStoffregen's Avatar
    Join Date
    Nov 2012
    Posts
    22,077
    The ordinary way to use pointers involves declaring them with the specific type of variable they access. Normally type casting should not be used with pointers. In a typical program "void *" (a pointer that doesn't specify what type of variable) should not be used.

    For this specific case, since you know that variable at the location the pointer specifies is "int", the pointer should be declared as "int *", like this:

    Code:
    void testClick(int *id) {
      Serial.println(*id, DEC);
    }
    There certainly are exceptional cases where "void *" and pointer type casting makes sense. But those sorts of situations are the exception, not the rule. That sort of programming is difficult and error prone, even for experts. Just because you can do a thing does not necessarily mean you should.

    When the type of variable at the location indicated by the pointer is clearly defined, which is almost always the situation for ordinary (not device driver) programming, you definitely should declare the pointer specifically for that type. Initially this can seem frustrating because you might get errors or warning from the compiler for wrong types. But those sorts of error messages are trying to help you. Don't "solve" them by using void or type casting, unless absolutely necessary.

    Also as a general guideline, when you know a pointer will be used to only read the actual variable, best practice is to declare it with const. While this isn't usually necessary, it gives you some extra protection (compiler errors) if you accidentally do something that uses the pointer in a way where it could write to the variable.

  9. #9
    Thanks for the detailed reply. After spending a few hours last night reading more about pointers I concluded that void pointers could be dangerous. In this case the state machine button library that I really like using implements the callback functionality with a method that accepts a function pointer and the void pointer parameter.

  10. #10
    Quote Originally Posted by PaulStoffregen View Post
    There certainly are exceptional cases where "void *" and pointer type casting makes sense. But those sorts of situations are the exception, not the rule. That sort of programming is difficult and error prone, even for experts. Just because you can do a thing does not necessarily mean you should.
    I have been thinking about this some more....

    I may look at re-writing the OneButton library but for now I'd like to know if there is any other way of doing what I am attempting to do.

    The OneButton library takes a parameter to its attachClick method as below:

    Code:
    // save function for click event
    void OneButton::attachClick(callbackFunction newFunction)
    {
      _clickFunc = newFunction;
    } // attachClick
    
    // save function for parameterized click event
    void OneButton::attachClick(parameterizedCallbackFunction newFunction, void* parameter)
    {
      _paramClickFunc = newFunction;
      _clickFuncParam = parameter;
    } // attachClick
    And the typedef for the above pointer types:
    Code:
    typedef void (*callbackFunction)(void);
    typedef void (*parameterizedCallbackFunction)(void*);
    It can ether take a straight function pointer with no parameter tat will just run a callback function by itself, or it can take the parameterizedCallbackFunction with the additional void pointer. From what Paul posted above this is where it gets messy in that, in my case, I know I will be passing an int pointer but the class method could just as easily take an array pointer for example.

    Is there a better way of doing this?

    Is there any other way to have a callback function execute the callback WITH the dynamic parameters that may include different parameter types?

    Should I modify the OneButton library to contain the callback types I will use? ie. do I add:
    Code:
    typedef void (*parameterizedCallbackFunction)(int*);
    typedef void (*parameterizedCallbackFunction)(long*);
    **update** that didn't work. I thought I could 'override' the typedef:
    Code:
    /Users/xxxx/Documents/Arduino/libraries/OneButton/OneButton.h:33:51: error: conflicting declaration 'typedef void (* parameterizedCallbackFunction)(int*)'
     typedef void (*parameterizedCallbackFunction)(int*);
                                                       ^
    /Users/xxxx/Documents/Arduino/libraries/OneButton/OneButton.h:32:16: note: previous declaration as 'typedef void (* parameterizedCallbackFunction)(void*)'
     typedef void (*parameterizedCallbackFunction)(void*);
                    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    One last question... when a parameterizedCallbackFunction variable is instantiated:

    Code:
    typedef void (*parameterizedCallbackFunction)(void*);
    
    parameterizedCallbackFunction exampleCallback;
    how does the compiler know how much memory to allocate to the variable given the void* parameter? Or is it as simple as "the instantiation of the exampleCallback variable results in memory required to hold the two pointer addresses"?
    Last edited by ilium007; 04-27-2020 at 01:45 AM.

  11. #11
    Senior Member PaulStoffregen's Avatar
    Join Date
    Nov 2012
    Posts
    22,077
    Quote Originally Posted by ilium007 View Post
    how does the compiler know how much memory to allocate to the variable given the void* parameter? Or is it as simple as "the instantiation of the exampleCallback variable results in memory required to hold the two pointer addresses"?
    On 32 bit processors (Teensy LC, 3.x, 4.x) any type of pointer is always 4 bytes. The pointer is just the numerical location where something else is located in the machine's memory space. So the same size number is used regardless of what type of thing the pointer actually references. On 64 bit computers (most PCs these days) when running 64 bit software, pointers are 8 bytes. The size of the pointer itself is determined by computer's maximum memory space.

    How many bytes the compiler will read from that location depends on what type of variable the pointer is declared to reference. For an "int *", the compiler will read 4 bytes as a signed integer. For a "char *" is reads 8 bits.

    For "void *" attempting to read is an error, because the compiler doesn't know what is supposed to actually be at that location. The size of the thing it points to is unknown or undefined for a void pointer. A void pointer can't be used to actually access the variable. That's why you need to type cast it to another a different pointer type, so the compiler can know how to actually access the variable.

    One of the uses for void pointers is with callback functions. Suppose a library provides a way for a function to be called when an event occurs. Your usage might be to update an integer. But maybe someone else wants the event to change a string. Someone else might be using an array of floats. Usually generic callback setup takes a void pointer, so it can be used with any type of data. Then in your function that gets called, you get the void pointer, because the library was written without any knowledge of what type of data you will use. Usually the very first thing you do is declare a pointer to the type of data you're actually using, and assign it to have the value of the void pointer. Then you use your pointer to actually access your data.

  12. #12
    That’s awesome. Thanks for your patience and excellent explanation.

    For my testing this is what the callback looks like now:

    Code:
    void callbackClick(void *buttonId) {
        int* _buttonId = ((int*) buttonId);  //  cast the void pointer to an int pointer
        Serial.print("Button ");
        Serial.print( *_buttonId, DEC);  //  de-reference the int pointer
        Serial.println(" clicked");
    }
    Last edited by ilium007; 04-27-2020 at 05:15 AM.

  13. #13
    Senior Member
    Join Date
    Apr 2014
    Location
    Germany
    Posts
    935
    I just want to add that using a bit of modern c++ you can easily work around that void* interface by using lambda expressions. The good thing is that this doesn't even need the parameterizedCallbackFunction but works with standard void(*callback)() type of callbacks. I.e. you can use this pattern for all libs providing only a void(*callback)() interface (e.g. IntervalTimer...)

    Code:
    #include "OneButton.h"
    
    OneButton button1(0, true, true);
    OneButton button2(1, true, true);
    OneButton button3(2, true, true);
    
    void myIntCallback(int cnt) // normal function taking an int parameter cnt and flashing cnt times
    {
      for (int i = 0; i < cnt; i++)
      {
        digitalWriteFast(LED_BUILTIN, HIGH);
        delay(50);
        digitalWriteFast(LED_BUILTIN, LOW);
        delay(200);
      }
    }
    
    void myFloatCallback(float x)
    {
      Serial.println(sin(x));
    }
    
    int someInt = 17;
    
    void setup()
    {
      pinMode(LED_BUILTIN, OUTPUT);
    
      button1.attachClick([] { myIntCallback(3); });       // Attach with int literal
      button2.attachClick([] { myIntCallback(someInt); }); // Attach with global int
      button3.attachClick([] { myFloatCallback(2.45f); }); // Attach float callback
    }
    
    void loop()
    {
      button1.tick();
      button2.tick();
      button3.tick();
    
      delay(10);
    }

  14. #14
    Awesome!! Lamdas are something I have heard people talk about but hadn't yet investigated. I will pull this apart tonight. I love this forum!

  15. #15
    Trying to get the Lamda functions working but need to pass in the int value as below. I have been reading about the need to 'capture' variables passed in to the Lamda but I can't get it to work:

    Code:
    for (uint8_t i = 0; i < numIdButtons; i++) {
        idButtons[i].begin(i);
        idButtons[i].attachClick([=](int i) {buttonIntCallback(i); });
      }
    Code:
    /Users/xxx/Documents/Arduino/CANButton/CANButton.ino: In function 'void setup()':
    CANButton:46:65: error: no matching function for call to 'IdButton::attachClick(setup()::<lambda(int)>)'
         idButtons[i].attachClick([=](int i) {buttonIntCallback(i); });
                                                                     ^
    /Users/xxx/Documents/Arduino/CANButton/CANButton.ino:17:10: note: candidate: void IdButton::attachClick(ButtonCallback)
         void attachClick(ButtonCallback f) {
              ^~~~~~~~~~~
    /Users/xxx/Documents/Arduino/CANButton/CANButton.ino:17:10: note:   no known conversion for argument 1 from 'setup()::<lambda(int)>' to 'ButtonCallback {aka void (*)()}'
    exit status 1
    no matching function for call to 'IdButton::attachClick(setup()::<lambda(int)>)'

  16. #16
    Quote Originally Posted by luni View Post
    I just want to add that using a bit of modern c++ you can easily work around that void* interface by using lambda expressions. The good thing is that this doesn't even need the parameterizedCallbackFunction but works with standard void(*callback)() type of callbacks. I.e. you can use this pattern for all libs providing only a void(*callback)() interface (e.g. IntervalTimer...)

    Using your code example I tried to access a variable inside the for loop. If I try to access 'buttonId' it won't compile but it is happy to access the global variable 'someInt'

    Code:
    #include "OneButton.h"
    
    void myIntCallback(int cnt) // normal function taking an int parameter cnt and flashing cnt times
    {
      for (int i = 0; i < cnt; i++)
      {
        digitalWrite(LED_BUILTIN, HIGH);
        delay(50);
        digitalWrite(LED_BUILTIN, LOW);
        delay(50);
      }
    }
    
    int someInt = 5;
    
    OneButton buttons[2];
    
    void setup()
    {
      Serial.begin(9600);
      
      pinMode(LED_BUILTIN, OUTPUT);
    
      for (int i=0; i < 2; i++) {
        int buttonId = ((i+4));
        buttons[i].begin(buttonId,true, true);
    
         //buttons[i].attachClick([] { myIntCallback(someInt); });
        buttons[i].attachClick([]() { myIntCallback(buttonId); });
      }
    }
    
    void loop()
    {
      for (int i=0; i < 2; i++) {
        buttons[i].tick();
      }
    }

    Code:
    /Users/xxx/Documents/Arduino/sketch_apr27d/sketch_apr27d.ino: In lambda function:
    sketch_apr27d:29:49: error: 'buttonId' is not captured
         buttons[i].attachClick([]() { myIntCallback(buttonId); });
                                                     ^~~~~~~~
    /Users/xxx/Documents/Arduino/sketch_apr27d/sketch_apr27d.ino:29:29: note: the lambda has no capture-default
         buttons[i].attachClick([]() { myIntCallback(buttonId); });
                                 ^
    /Users/xxx/Documents/Arduino/sketch_apr27d/sketch_apr27d.ino:25:9: note: 'int buttonId' declared here
         int buttonId = ((i+4));
             ^~~~~~~~
    exit status 1
    'buttonId' is not captured
    Last edited by ilium007; 04-27-2020 at 08:56 AM.

  17. #17
    Senior Member
    Join Date
    Apr 2014
    Location
    Germany
    Posts
    935
    That won't work with the OneButton library as it is.

    Reason:
    You can only have a lambda as a replacement for a void(*)() function if the lambda doesn't capture variables. Otherwise the type of the lambda is not compatible to void(*)() which is what your library expects (and what the compiler complains about). If you can't or don't want to pass the buttonId as a constant or global variable this approach simply doesn't work.

    Anyway, out of interest I'll have a look if a small tweak of the library will fix this.

  18. #18
    Quote Originally Posted by luni View Post
    You can only have a lambda as a replacement for a void(*)() function if the lambda doesn't capture variables.
    Ahh yes, that old chestnut :|

  19. #19
    Senior Member
    Join Date
    Apr 2014
    Location
    Germany
    Posts
    935
    So, the good thing is that the lib authors did use a typedef for the callback types. Thus, the lib can be rejuvenated by a very simple surgical procedure. Just replace the typedef for the callback function by the more modern std::function

    Code:
    // starting at line 23:
    #ifndef OneButton_h
    #define OneButton_h
    
    #include "Arduino.h"
    #include <functional>   <============= ADD
    
    // ----- Callback function types -----
    
    extern "C" {
    //typedef void (*callbackFunction)(void);
    using callbackFunction = std::function<void(void)>;  // <======= replace dumb function pointer by std::function
    typedef void (*parameterizedCallbackFunction)(void*);
    }
    /....
    After this changes you can throw more or less anything callable at the lib (function pointers, functors, member functions etc). In particular your lambdas with captured variables will work without problems. (BTW, it seems like you use a slightly different OneButton, because mine doesn't have a begin(); I therefore did the initialization of the buttons directly in the array definition.)

    Code:
    #include "OneButton.h"
    
    OneButton idButtons[]{// tree buttons on pins 0,1 and 2
        {0, true, true},
        {1, true, true},
        {2, true, true}
    };
    
    constexpr unsigned numIdButtons = sizeof(idButtons) / sizeof(idButtons[0]);
    
    void buttonIntCallback(int cnt) // normal function taking an int parameter cnt and flashing cnt times
    {
      for (int i = 0; i < cnt; i++)
      {
        digitalWriteFast(LED_BUILTIN, HIGH);
        delay(50);
        digitalWriteFast(LED_BUILTIN, LOW);
        delay(200);
      }
    }
    
    void setup()
    {
      pinMode(LED_BUILTIN, OUTPUT);
    
      for (unsigned i = 0; i < numIdButtons; i++)
      {
        idButtons[i].attachClick([i] { buttonIntCallback(i + 1); });
      }
    }
    
    void loop()
    {
      for (unsigned i = 0; i < numIdButtons; i++)
      {
        idButtons[i].tick();
      }
    
      delay(10);
    }

  20. #20
    That works and compiles for the Teensy. Thanks. I also wanted to use this for some of the smaller boards that run ATMega328's but functional is not available:

    Code:
    fatal error: functional: No such file or directory

  21. #21
    Senior Member
    Join Date
    Apr 2014
    Location
    Germany
    Posts
    935
    No, that doesn't make sense on a 8bit machine. I suggest to stick with the function pointers as shown in #12

  22. #22
    Quote Originally Posted by luni View Post
    No, that doesn't make sense on a 8bit machine. I suggest to stick with the function pointers as shown in #12
    Thanks, I'll do that. It keeps things I build usable across ATMega's and the Teensy's.

  23. #23
    But... I could go down this path instead of my custom ATMega328 boards

    https://www.pjrc.com/store/ic_mkl02.html

  24. #24
    Thanks again for all the help!

  25. #25
    Senior Member
    Join Date
    Feb 2017
    Posts
    413
    Something like this will work on any Arduino board and you won't have to resort to templates or lambdas:
    Code:
    #include "Arduino.h"
    #include "OneButton.h"
    
    class NewButton: public OneButton {
    public:
    	NewButton(uint8_t p) : OneButton(p, true), pin(p) {
    		OneButton::attachClick(callBack, this);
    	}
    
    private:
    	uint8_t pin;
    
    	void doSomething() {
    		Serial.println(pin);
    	}
    
    	static void callBack(void *p) {
    		((NewButton *) p)->doSomething();
    	}
    
    };
    
    NewButton buttons[] = {8, 9, 10};
    uint8_t numButtons = sizeof(buttons) / sizeof(buttons[0]);
    
    void setup() {
    
    }
    
    void loop() {
    	for (uint8_t i=0; i<numButtons; i++) {
    		buttons[i].tick();
    	}
    }

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •