Best approach to add sgn/sign/signum definition to Arduino.h (or not)

1,060 views
Skip to first unread message

David Brown

unread,
Jul 19, 2019, 7:18:53 PM7/19/19
to Developers
I recently needed the sign of a number, but there is no convenient "function" for this in the Arduino core, so I added
#define sgn(x) ((x) < 0 ? -1 : ((x) > 0 ? 1 : 0))
to my .ino file, following the example of abs and constrain defines I found in Arduino.h.

It seems like this would be a fairly commonly used capability and I'd be happy to create a pull request, but I thought I'd ask here first...
  1. would such an enhancement be appropriate, or would it not be wanted in any form to minimize the number of reserved words/tokens already defined?
  2. would a different name such as sign (to maximize English-language readability) or signum (to hopefully minimize conflicts) be preferred?
  3. if such a pull request would be welcome, is https://github.com/arduino/ArduinoCore-API/blob/master/api/Common.h the right place to add it?
  4. We'd also want a new page in https://github.com/arduino/reference-en/tree/master/Language/Functions/Math... is there a how-to for contributions that affect multiple repositories?
Thanks,
Dave

Rob Tillaart

unread,
Jul 19, 2019, 9:13:33 PM7/19/19
to Arduino Developers
Hi David

There is a problem with the definition of sign(x) this way and that is that x can be evaluated twice in the macro. Example

X = 1
Y = sign(--x)
Print x

Could print -1. 

So sign() should either be a (overloaded)  function or a macro in which x will only be evaluated once. I use the function to prevent this error.

That said I support the proposal.of adding a sign() function 

Rob




--
You received this message because you are subscribed to the Google Groups "Developers" group.
To unsubscribe from this group and stop receiving emails from it, send an email to developers+...@arduino.cc.
To view this discussion on the web visit https://groups.google.com/a/arduino.cc/d/msgid/developers/5e5c1fd0-f54d-4591-a335-9237f3f2381d%40arduino.cc.

Roger Irwin

unread,
Jul 22, 2019, 2:00:46 AM7/22/19
to devel...@arduino.cc
A C style function implementation of C++ Sgn ?


Paul Stoffregen

unread,
Jul 22, 2019, 4:48:54 AM7/22/19
to devel...@arduino.cc

> There is a problem with the definition of sign(x) this way and that is
> that x can be evaluated twice in the macro.

Pretty sure some of the long-established macros in Arduino.h also have
this side effect, like max(), min(), abs(), constrain(), round(), sq().

david gauchard

unread,
Jul 22, 2019, 5:11:33 AM7/22/19
to Developers
FWIW, there are several proposals for a fix:

#include <stdio.h>

// multi-evaluation (bad)
// C / C++
#define sgn0(x) ((x) < 0 ? -1 : ((x) > 0 ? 1 : 0))

// C / C++
#define sgn1(x) ({ __typeof__(x) _x = (x); _x < 0 ? -1 : _x ? 1 : 0; })

// C++
#define sgn2(x) ({ decltype(x) _x = (x); _x < 0 ? -1 : _x ? 1 : 0; })

// C++
template <typename T>
inline T sgn3 (T x) { return x < 0 ? -1 : x ? 1 : 0; }

int main (void)
{
    printf("result should be -1 0 1\n");
    for (int x = -1; x <= 1; x++)
    {
        { int y = x; printf("%-3d ", sgn0(y++)); }
        { int y = x; printf("%-3d ", sgn1(y++)); }
        { int y = x; printf("%-3d ", sgn2(y++)); }
        { int y = x; printf("%-3d ", sgn3(y++)); }
        printf("\n");
    }
    return 0;
}

/* result:
result should be -1 0 1
-1  -1  -1  -1
1   0   0   0
1   1   1   1

sgn0 is not good
*/

Rob Tillaart

unread,
Jul 22, 2019, 5:27:50 AM7/22/19
to Arduino Developers
@david
Thanks for testing,
Could you add to your test the floats
+0.0
-0.0
+NaN
-NaN

As these are special values within IEEE754 I expect +0.0 and -0.0 should return 0 and the NAN should return itself. But I don't know the consensus / specs on these values. 


--
You received this message because you are subscribed to the Google Groups "Developers" group.
To unsubscribe from this group and stop receiving emails from it, send an email to developers+...@arduino.cc.

david gauchard

unread,
Jul 22, 2019, 5:54:50 AM7/22/19
to Developers
With another template version returning int, and float limits:


#include <stdio.h>

// multi-evaluation (bad)
// C / C++
#define sgn0(x) ((x) < 0 ? -1 : ((x) > 0 ? 1 : 0))
// C / C++
#define sgn1(x) ({ __typeof__(x) _x = (x); _x < 0 ? -1 : _x ? 1 : 0; })
// C++
#define sgn2(x) ({ decltype(x) _x = (x); _x < 0 ? -1 : _x ? 1 : 0; })
// C++ (same type for result)

template <typename T>
inline T sgn3 (T x) { return x < 0 ? -1 : x ? 1 : 0; }
// C++ (int as a result)
template <typename T>
inline int sgn3bis (T x) { return x < 0 ? -1 : x ? 1 : 0; }

void sf (float x)
{  
    printf("%-3d ", sgn0(x));
    printf("%-3d ", sgn1(x));
    printf("%-3d ", sgn2(x));
    printf("%-3g ", sgn3(x));
    printf("%-3d ", sgn3bis(x));
    printf("\n");

}

int main (void)
{
    printf("result should be -1 0 1\n");
    printf("int (sgn(x++)):\n");

    for (int x = -1; x <= 1; x++)
    {
        { int y = x; printf("%-3d ", sgn0(y++)); }
        { int y = x; printf("%-3d ", sgn1(y++)); }
        { int y = x; printf("%-3d ", sgn2(y++)); }
        { int y = x; printf("%-3d ", sgn3(y++)); }
        { int y = x; printf("%-3d ", sgn3bis(y++)); }
        printf("\n");
    }

    printf("floats (sgn(x)):\n");
    sf(-1);
    sf(0);
    sf(1);
   
    float plusnan = 1.0 / 0.0;
    printf("%g\n", plusnan);
    sf(plusnan);
    float minusnan = -1.0 / 0.0;
    printf("%g\n", minusnan);
    sf(minusnan);
    printf("%g\n", 1.0 / plusnan);
    sf(1.0 / plusnan);
    printf("%g\n", -1.0 / plusnan);
    sf(-1.0 / minusnan);


    return 0;
}

/* result:
result should be -1 0 1
int (sgn(x++)):
-1  -1  -1  -1  -1 
1   0   0   0   0  
1   1   1   1   1  
floats (sgn(x)):
-1  -1  -1  -1  -1 
0   0   0   0   0  
1   1   1   1   1  
inf
1   1   1   1   1  
-inf
-1  -1  -1  -1  -1 
0
0   0   0   0   0  
-0
0   0   0   0   0  

sgn0 is not good even when result is good because parameter is evaluated multiple times
*/

Adrian Godwin

unread,
Jul 22, 2019, 6:51:37 AM7/22/19
to devel...@arduino.cc
Unexpected multi-evaluation is not good. But neither is unexpected variable allocation or copying.

The compromise used in C for the last 40 years or so is to educate the programmers to know that max() when a macro might cause multi evaluation, and that they should consider this. If the alternative is that some other side effect is created, even if modern processors make that fairly lightweight, I still feel it's something the programmer should understand. There is no such thing as an effect-free method (or we would have done it by now) - only a different effect.

In addition, what would you do with the result when it's trivalued ? You can't test it directly with an if or a ternary operator. You have to consider all three outcomes. This usually means you care only about two of the outcomes, but the code you write will have to choose those two. So you might as well just code it directly with <,>,<=,>=. Or if all states need to be considered, an if . .else if .. else - which again can explicitly use multiple evaluations or buffer as appropriate.

Can you give examples of how such a trivalued function or macro would be used, and how it aids clarity, efficiency or reliability ?


--
You received this message because you are subscribed to the Google Groups "Developers" group.
To unsubscribe from this group and stop receiving emails from it, send an email to developers+...@arduino.cc.

kpe...@blinksoft.com

unread,
Jul 22, 2019, 6:53:49 AM7/22/19
to devel...@arduino.cc

Why wouldn’t I use a boost header only math library for these functions rather than making it part of Arduino?

 

Ken

david gauchard

unread,
Jul 22, 2019, 8:30:52 AM7/22/19
to Developers
> I still feel it's something the programmer should understand.
> There is no such thing as an effect-free method (or we would have done it
> by now) - only a different effect.

The assembly code produced for atmega328p is identical with the three
versions of the following macro (using arduino-1.8.6, avr-gcc-5.4.0 -Os):

#define evmin(x,y) ((y) < (x)? (y): (x))
#define mymin(x,y) ({ __typeof__(x) _x = (x); __typeof__(y) _y = (y); _y < _x? _y: _x; })
template <typename T> inline T cppmin (T x, T y) { return y < x? y: x; }
int c = -1, d = 1;
void setup () {
  Serial.begin(9600);
  Serial.println(mymin(c, d));
  Serial.println(evmin(c, d));
  Serial.println(cppmin(c, d));
}

IMHO,

- modern compilers are able to optimize the code quite efficiently.  Of
  course without the option `-Os`, result might be different.

- the `evmin()` version is wrong for arduino users, who are considered as
  beginners and not veteran C programmers (even when it is spelled with
  capitals like MIN() so to be considered as a multi-evaluating macro).


> In addition, what would you do with the result when it's trivalued ? You
> can't test it directly with an if or a ternary operator. You have to
> consider all three outcomes. This usually means you care only about two of
> the outcomes, but the code you write will have to choose those two. So you
> might as well just code it directly with <,>,<=,>=. Or if all states need
> to be considered, an if . .else if .. else - which again can explicitly use
> multiple evaluations or buffer as appropriate.
>
> Can you give examples of how such a trivalued function or macro would be
> used, and how it aids clarity, efficiency or reliability ?

Please clarify what is "trivalued".
About reliability, `evmin()` is the worst of them.
About performance, current compiler produce the same assembly code.
About clarity, absolute beginners are not supposed to read sources, or
they are not beginners anymore.

I'm not pretending anything, nor teaching anyone.  What's above is well
known when googling around or when coding is a habit or a job.  Given the
number of messages there are on this thread on this very subject, I felt I
could bring something.

david gauchard

unread,
Jul 22, 2019, 8:46:40 AM7/22/19
to Developers
> Why wouldn’t I use a boost header only math library for these functions rather than making it part of Arduino?

Probably because rtti and exceptions are usually disabled on arduino, which makes it difficult to use std:: of boost:: to their full extent.

Adrian Godwin

unread,
Jul 22, 2019, 9:08:48 AM7/22/19
to devel...@arduino.cc
What I mean by 'trivalued' is that the function / macro returns +1, -1 or zero. Not just positive (which usually includes zero) or negative.

I'm interested to hear that the code produced with optimisation doesn't actually evaluate the argument twice, despite the suspicion that it might do so. But yes, it has long been said that compilers remove most surplus code. Though you wouldn't  know it from the horrendous startup times and footprints of many modern applications, despite their lack of useful gains over older ones!

--
You received this message because you are subscribed to the Google Groups "Developers" group.
To unsubscribe from this group and stop receiving emails from it, send an email to developers+...@arduino.cc.

kpe...@blinksoft.com

unread,
Jul 22, 2019, 9:10:14 AM7/22/19
to devel...@arduino.cc

Hmm interesting. I was not aware the math headers used exceptions in boost.  Will have to read through them again.  Thanks.

 

Ken

--

You received this message because you are subscribed to the Google Groups "Developers" group.
To unsubscribe from this group and stop receiving emails from it, send an email to developers+...@arduino.cc.

Mikael Patel

unread,
Jul 22, 2019, 10:06:55 AM7/22/19
to Developers
Yet another template/auto based version with a slight twist. Cheers! Mikael

#if (__cplusplus > 201400)
inline int sign(auto x)
{
  return ((x > 0) - (x < 0));
}
#endif

#define print_P(x) print(F(x))

template<typename T>
inline int sign(T x)
{
  return ((x > 0) - (x < 0));
}

void setup()
{
  Serial.begin(57600);

  Serial.print_P("__cplusplus = ");
  Serial.println(__cplusplus);

  Serial.print_P("sign(1) = ");
  Serial.println(sign(1));
  Serial.print_P("sign(0) = ");
  Serial.println(sign(0));
  Serial.print_P("sign(-1) = ");
  Serial.println(sign(-1));

  Serial.print_P("sign(1.0) = ");
  Serial.println(sign(1.0));
  Serial.print_P("sign(0.0) = ");
  Serial.println(sign(0.0));
  Serial.print_P("sign(-1.0) = ");
  Serial.println(sign(-1.0));
}

void loop()
{
}

david gauchard

unread,
Jul 22, 2019, 10:17:33 AM7/22/19
to Developers
> I'm interested to hear that the code produced with optimisation doesn't actually evaluate the argument twice, despite the suspicion that it might do so.

This is a tough question you ask.
Here's my try, with a c++ class emulating an `int` and logging all access (r, w, rw, compare) with the 4 `sgn` definitions, and the three result cases (-1,0,1) each.

We can see that the legacy definition of `sgn` (`sgn0`) evaluates the user argument multiple times, while only once with the others.
(this comment is not valid for the template which is a special case for this test)

Runned on arduino nano, results below:
---------------------------------
```
// offending definition
#define sgn0(x) ((x) < 0 ? -1 : ((x) > 0 ? 1 : 0))

#if 0
// we should use:

#define sgn1(x) ({ __typeof__(x) _x = (x); _x < 0 ? -1 : _x ? 1 : 0; })
#define sgn2(x) ({ decltype(x) _x = (x); _x < 0 ? -1 : _x ? 1 : 0; })
#else
// but for this test we use 'int' for _x internally so to only print out accesses to user's x which we really want to instrument
#define sgn1(x) ({ int _x = (x); _x < 0 ? -1 : _x ? 1 : 0; })
#define sgn2(x) ({ int _x = (x); _x < 0 ? -1 : _x ? 1 : 0; })
#endif

// c++ template version:
// there is no distinction between user input value and the processed one,
// so they have the same type and everything is logged. Compare this version with #if 1 above
template <typename T> inline int sgn3 (const T& x) { return x < 0 ? -1 : x ? 1 : 0; }

// note:
// without MyInt c++ wrapper, assembly code is exactly the same when argument is trivial for all sgn0123

class MyInt
{
protected:
  int x;
public:
  MyInt (int init): x(init) { Serial.print("(i)"); }
  int& operator = (int& init) { Serial.print("(w)"); x = init; return *this; }
  operator int () const { Serial.print("(r)"); return x; }
  operator int& () { Serial.print("(rw)"); return x; }
  bool operator < (const MyInt& o) const { Serial.print("(r<)"); return x < o.x; }
  bool operator == (const MyInt& o) const { Serial.print("(r==)"); return x == o.x; }
  bool operator < (int o) const { Serial.print("(r<)"); return x < o; }
  bool operator == (int o) const { Serial.print("(r==)"); return x == o; }
};

MyInt a = -1, b = 0, c = 1;

#define __STRINGIFY(x) #x
#define C(n,v) \
  Serial.print("sgn" __STRINGIFY(n) " with " __STRINGIFY(v) "="); \
  Serial.println((const int)v); \
  Serial.println(sgn##n(v));
#define T(n) Serial.println("------"); C(n, a) C(n, b) C(n, c)
 
void setup() {
  Serial.begin(115200);
  T(0) T(1) T(2) T(3)
}

void loop() {
}

#if 0
Results:

sgn0 with a=(rw)-1
(r<)-1
sgn0 with b=(rw)0
(r<)(rw)0
sgn0 with c=(rw)1
(r<)(rw)1
------
sgn1 with a=(rw)-1
(rw)-1
sgn1 with b=(rw)0
(rw)0
sgn1 with c=(rw)1
(rw)1
------
sgn2 with a=(rw)-1
(rw)-1
sgn2 with b=(rw)0
(rw)0
sgn2 with c=(rw)1
(rw)1
------
sgn3 with a=(rw)-1
(r<)-1
sgn3 with b=(rw)0
(r<)(r)0
sgn3 with c=(rw)1
(r<)(r)1
#endif
```

Antoine Laps

unread,
Jul 22, 2019, 3:46:41 PM7/22/19
to devel...@arduino.cc
Why not use a simple bit shifting?

A function like this:

(x & (1u << (sizeof(x) <<3))

This simple ligne return 1 if x is negative 

--
You received this message because you are subscribed to the Google Groups "Developers" group.
To unsubscribe from this group and stop receiving emails from it, send an email to developers+...@arduino.cc.

Adrian Godwin

unread,
Jul 22, 2019, 4:38:01 PM7/22/19
to devel...@arduino.cc
1. I don't believe it will work on floats / doubles
2. I'm not sure I see the advantage over (x < 0)

Ian Katz

unread,
Jul 22, 2019, 4:39:31 PM7/22/19
to devel...@arduino.cc
This seems like a case where it would be better to enumerate the
desired compilation tests (e.g. what data types may use this function)
& unit tests (e.g. the "--x" referenced above), then pick the
implementation that satisfies them. From there, let measured
performance pick the winner.
> To view this discussion on the web visit https://groups.google.com/a/arduino.cc/d/msgid/developers/CAD7Ck5Apoq3x55mt-qjbPGA7RrLGJONwL4%2B%2Btg1ARZ_3Jj5QyA%40mail.gmail.com.

David Brown

unread,
Jul 31, 2019, 9:17:52 PM7/31/19
to Developers
Just wanted to thank everyone for their insights, advice, opinions, and suggestions. I do more of my programming in languages such as PHP and JavaScript which don't really have anything like the C preprocessor, so while I had read the caution about sqr and constrain in the documentation, seeing all the different ways to do something like this and their trade-offs was quite instructive.

There certainly doesn't seem to be any consensus about how to go about adding this so to the built-in list, so I won't pursue that.

Thanks!
David

Gabriel Staples

unread,
Aug 7, 2019, 7:41:42 PM8/7/19
to devel...@arduino.cc
I'd like to point out that gcc offers an extension called "statement expressions" which solves this problem.

See this answer here: https://stackoverflow.com/a/3533300/4561887
and official documentation here: https://gcc.gnu.org/onlinedocs/gcc/Statement-Exprs.html

The official documentation even uses `max()` as their exact demonstration to show how to use it and how it is beneficial. Here's a screenshot of that usage and its benefits. In short, instead of the traditional:

#define max(a,b) ((a) > (b) ? (a) : (b))

which has this problem, you use gcc's statement expression syntax:

#define maxint(a,b) \
({int _a = (a), _b = (b); _a > _b ? _a : _b; })

which does not.
image.png

Thanks!

Gabriel Staples

Electric RC Aircraft Guy, LLC

Check out my Computa Pranksta USB device sold on Amazon!



--
You received this message because you are subscribed to the Google Groups "Developers" group.
To unsubscribe from this group and stop receiving emails from it, send an email to developers+...@arduino.cc.

Gabriel Staples

unread,
Aug 7, 2019, 7:43:46 PM8/7/19
to devel...@arduino.cc
...whatever statement you have at the end of a "statement expression" macro acts like a return value, as though it was from an inline function, except it gets the speed and other syntax benefits of a macro.


Thanks!

Gabriel Staples
Electric RC Aircraft Guy, LLC

Check out my Computa Pranksta USB device sold on Amazon!


Gabriel Staples

unread,
Aug 7, 2019, 7:50:35 PM8/7/19
to devel...@arduino.cc
Sorry, I forgot. You must handle ambiguous types passed to `max()`, so here is the final version using the `typeof` keyword:

#define max(a,b) \ ({ typeof (a) _a = (a); \ typeof (b) _b = (b); \ _a > _b ? _a : _b; })

Thanks!

Gabriel Staples
Electric RC Aircraft Guy, LLC

Check out my Computa Pranksta USB device sold on Amazon!

Reply all
Reply to author
Forward
0 new messages