nothing_but<T>

Value Handle to Avoid Implicit Conversions in standard C++

Update 2019 Dec

Moved to its own repository here.

Reddit discussion is here.

Introduction

“… C++ is not a bad language per se. It’s just, well kinda suffers from bloat. In the words of Richard E. Gooch, “It seduces the programmer, making it much easier to write bloatware“…”

Probably you indulge into recreational C++ and the following is just a light curiosity for you ( https://godbolt.org/z/u0fkZo ) :

#include <stdint.h> // try: // 1: -Wsign-conversion -O3 // 2: -Wall int main () { // signed to unsigned ? uint8_t uc = (int8_t)42; // unsigned to signed ? int8_t sc = uc; // never warns // char to unsigned ? uc = '!'; // char to signed ? sc = '!'; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <stdint.h> // try: // 1: -Wsign-conversion -O3 // 2: -Wall int main ( ) { // signed to unsigned ? uint8_t uc = ( int8_t ) 42 ; // unsigned to signed ? int8_t sc = uc ; // never warns // char to unsigned ? uc = '!' ; // char to signed ? sc = '!' ; }

That is also a C code. In any case, modern compilers give little or no warnings. Unless you use -Wall . Even then only unsigned to signed assignment produces a warning.

Or. Perhaps you are not a fan of “recreational programming” and you do take the above so seriously you actually do not use C++ on mission-critical projects? Perhaps your code is firmware that has to be installed, inside some medical equipment your company delivers?

You might take the C++ implicit conversions so seriously that “even” the following is a very serious matter for you.

// implicit conversion of int to char char C = 64 ; // implicit conversion of double to float float F = 3.7; 1 2 3 4 // implicit conversion of int to char char C = 64 ; // implicit conversion of double to float float F = 3.7 ;

Not because you happen to be “unreasonable”, but because you need to deliver code where implicit conversion is simply not allowed. Just like for example exceptions, in many real-time projects are not allowed. They simply do not exist over there. Them exceptions are simply switched off.

In the world of C++, there is no switch to stop implicit conversions.

Here is yet another (not that) funny example:

#include <iostream> int main() { int i = -1; unsigned int j = 1; if ( i < j ) std::cout << " i is less than j"; else std::cout << " i is greater than j"; return 0; } 1 2 3 4 5 6 7 8 9 10 11 #include <iostream> int main ( ) { int i = - 1 ; unsigned int j = 1 ; if ( i < j ) std :: cout << " i is less than j" ; else std :: cout << " i is greater than j" ; return 0 ; }

The output?

i is greater than j 1 i is greater than j

Not laughing anymore. And very likely, you have already turned to the “official sources” just to come back disappointed. That advice are, to put it mildly, not feasible, in real life, non-trivial projects.

Interestingly, some other coding platforms, do have exact types , should we say “for ages”. Alas, never part of C++ or C.

One option is to plan for the usual extended test/debug/test/debug, cycles ad-infinitum, of course. Spending a **lot** of time chasing this kind of bugs.

Before you dump C++, can we offer to you

The suggestion

So, before you start learning Ada and discard (with the heavy heart) C++ for your mission-critical projects, we might perhaps suggest you look into this ridiculously tiny single standard C++ header? Here is some code to tickle your fancy.

// let's assume your code must not accidentally mix // signed and unsigned chars using just_signed = dbj::util::nothing_but<signed char> using just_unsigned = dbj::util::nothing_but<unsigned char> 1 2 3 4 5 6 // let's assume your code must not accidentally mix // signed and unsigned chars using just_signed = dbj :: util :: nothing_but < signed char > using just_unsigned = dbj :: util :: nothing_but < unsigned char >

Just declarations first. As ever, make default initialized content, but with a twist: of exactly the types required.

// signed char 0 just_signed s; // unsigned char 0 just_unsigned u; 1 2 3 4 // signed char 0 just _ signed s ; // unsigned char 0 just _ unsigned u ;

Now comes the interesting part.

To actually assign anything to these types you must very consciously make those types first.

// using MSVC typedefs here // and C++ style cast s = int8_t('s'); u = uint8_t('u'); 1 2 3 4 // using MSVC typedefs here // and C++ style cast s = int8_t ( 's' ) ; u = uint8_t ( 'u' ) ;

You or your team simply can not introduce a bug there. The following will simply not compile.

s = 's'; // does not compile u = 'u'; // does not compile s = u; // does not compile s == u; // does not compile 1 2 3 4 s = 's' ; // does not compile u = 'u' ; // does not compile s = u ; // does not compile s == u ; // does not compile

Just a perfect API to avoid those nasty little pests growing into bugs very difficult to find.

Now, this might seem like “not a lot of code” to you, but we are just showing an API new to you. The above code looks almost too simple. It is easy to forget the safety service this API provides.

Now the really worn-out phrase: Your imagination is the limit. Very true here.

Type’s handled

This API does handle all the arithmetic types.

integral types bool char char16_t char32_t wchar_t signed char short int

int long int long long int unsigned char unsigned short int unsigned int unsigned long int unsigned long long int

floating-point types float double long double



These are the types where implicit conversions do happen, by design.

But what about compound types? For example what about:

References

Pointers

Arrays

Why not handle them too? Simply because, in case you need them you will naturally use them as ever before, but combined with this API.

// 0 of a signed char type just_signed signed_char ; // pointer to it just_signed * signed_char_pointer ; // null_ptr as ever_ // reference to it just_signed & signed_char_ref = signed_char ; // array of three elements of it just_signed signed_char_arr[3]_; 1 2 3 4 5 6 7 8 9 10 11 // 0 of a signed char type just_signed signed _ char ; // pointer to it just_signed * signed_char _ pointer ; // null_ptr as ever_ // reference to it just_signed & signed_char_ref = signed _ char ; // array of three elements of it just_signed signed_char_arr [ 3 ] _ ;

Please do note, how above, all the standard C++ default value initialization rules are respected.

In case of some serious bugs, or singularities discovered, or edge case, we will reconsider the currently handled types.

Going beyond arithmetics it is very unlikely the implicit conversion might be the problem in your code.

Dependencies

This API depends on C++ std lib only. We are developing using Visual Studio 2019, But we are always checking it is equally usable with both CLANG and GCC.

Installation

This API is header-only: it consists entirely of one header file:

dbj_nothing_but.h

No compilation is necessary. No installation required.

Just drop it in and include it. Make it part of your project.

Every attempt has been made to make this into a cross-platform, header only, standard C++ library.

At the time of this writing (2020-Q2) standard C++ is (still) C++17.

Builds and tests are passing with C++14 (/std:c++14)

Builds and tests are passing with C++17 (/std:c++17)

Contact

Please report issues or questions here.

You can contact me via twitter at @dbjdbj, or via email.

Contributing

Any feedback from users and stakeholders will be used to improve the library.

Copyright(C) 2019 Dušan B. Jovanović (dbj@dbj.org)

CC BY SA 4.0