6

If I'm using a library which currently uses a particular numeric type alias, e.g.

typedef uint32_t library_type;

void library_function(library_type x);

How would I ensure that code which needs to pass in a value from a different type remains correct, even if the library changes its typedef?

uint64_t x = get_some_number();

// …need checks here…

library_function((library_type)x);

I could add the following checks:

assert(sizeof library_type >= sizeof uint32_t);
assert(x <= UINT32_MAX);

The second check would ensure the value I get fits within the range of the current library_type. The library author does not provide a LIBRARY_TYPE_MAX definition, so the first check tries to guard the second, in case library_type were to change for whatever reason when the code is compiled in the future.

That first check would catch if library_type were change to e.g. int8_t, but what if the library_type were changed to an int32_t instead? It's still the right "size" but the range is still less than what I am checking!

Does the C language provide an operator that introspects the signedness of a type, in the same way that sizeof lets me know the width? Is there any other way to make sure my cast to library_type will only be reached if it is correct?

natevw
  • 16,807
  • 8
  • 66
  • 90
  • 2
    You are confusing runtime and compile time checks. C doesn't really do dynamic types at runtime (unless you build in some scheme of your own for storing the type which should be used to re-interpret what C would originally view as raw data), so any check you do to determine if the source code has been changed would have to be at compile time, not a runtime check like assert(). – Chris Stratton Dec 12 '19 at 19:27
  • Looks like C++11 provides `std::is_signed`; here I'm wondering if there's anything similar in C (and also whether this is the right approach at all ;-) – natevw Dec 12 '19 at 19:28
  • The right approach is to make sure this can't happen. Most libraries define their own types. If someone edits the source code, they rather than you own responsibility. Though it is possible for a build script to build in some sense of integrity against non-malicious actors, for example capture its own git state in a version field. – Chris Stratton Dec 12 '19 at 19:30
  • @ChrisStratton For the purposes of the question, I don't care if it's a runtime check or a compile time check. My `assert(x <= …)` check would need to be runtime; making sure *that* check is correct could be compile time or runtime. – natevw Dec 12 '19 at 19:30
  • 1
    Are you trying to protect against mistaken edits of the library's source code, or mistaken parameters passed to the library at runtime? Neither problem is particularly solvable, but the question is really unanswerable until you clarify your exact fear. – Chris Stratton Dec 12 '19 at 19:32
  • @ChrisStratton Think of my question as "how can I ensure a [numeric] cast remains correct when I have no current/future control over the type I am casting to?" – natevw Dec 12 '19 at 19:34
  • 3
    Can you not remove the explicit cast and then catch incorrect implicit conversions with `-Wconversion`? Get something like: `conversion to uint32_t {aka unsigned int} from int32_t {aka int} may change the sign of the result` – kaylum Dec 12 '19 at 19:38
  • @kaylum Hmm, there might be a way but the cast is already necessary afaict since I'm going from a `uint64_t` (which "probably" doesn't store a big number in practice) to something smaller. – natevw Dec 12 '19 at 19:41
  • 2
    Well then cast it to the type you want checked before calling the function. `uint32_t x_cast = (uint32_t) x; library_function(x_cast);` – kaylum Dec 12 '19 at 19:45
  • @kaylum Yes! I think that's the solution, and nicer than my own idea which was to only cast if within `INTn_MAX` instead of `UINTn_MAX`. – natevw Dec 12 '19 at 20:01

2 Answers2

6

Yes, the relational operators combined with cast operator. For example:

_Bool TYPE_is_signed = ((TYPE)-1 < 0);

Or as an assertion:

assert((TYPE)-1 < 0); // Require that TYPE is signed
assert((TYPE)-1 > 0); // Require that TYPE is unsigned
R.. GitHub STOP HELPING ICE
  • 208,859
  • 35
  • 376
  • 711
  • 1
    Neat, and could be turned into a macro if needed. In practice I might prefer @kaylum's solution of using an intermediate cast to the **current** underlying type (e.g. cast to `uint32_t` instead of `library_type`) and let compiler catch any breaking changes. But this answers my question as posed. – natevw Dec 12 '19 at 20:08
  • 1
    `assert(((TYPE1)-1 < 0) == ((TYPE2)-1 < 0));` – R.. GitHub STOP HELPING ICE Dec 12 '19 at 20:11
2

How would I ensure that code which needs to pass in a value from a different type remains correct, even if the library changes its typedef?

The situation you are trying to defend against is one of irresponsible human behavior rather than a technical one.

Changing a typedef of an argument in an API is a "breaking change" - just as any other change in behavior should be. Such a change is, if more than a bugfix, also likely a hint at others which may be more subtle and severe.

Breaking changes should be documented in release notes.

Typically, if there is automated enforcement of a breaking change in an API, it is by means such as version identifiers or capability queries which can be checked at build and/or runtime - somewhere in the library headers would be #define LIBRARY_WHATEVER_VERSION 3 and then your code can have preprocessor directives to check that.

When a breaking change is really severe, often the software itself is renamed such that the wrong version would not even satisfy #include or link attempts.

Chris Stratton
  • 39,853
  • 6
  • 84
  • 117
  • 2
    I think you're getting distracted by the design of this hypothetical library, which happens to be controlled by a huge corporation that cares nothing for our opinions. Can I write portable C code which detects if a number can be cast to a smaller but otherwise ± unknown type? – natevw Dec 12 '19 at 19:59
  • 1
    I think you're getting distracted by one breaking change and ignoring others. And your provider isn't going to just change things without bumping a version number. Take time to understand the changes before you start using a new version! Have your build system check an md5sum of the headers if there is no encoded versioning and you think a co-worker might be sloppy in setting up a build. Better yet, capture all the vendor deliverables into a git submodule, even if they just dump header + binary zipfiles on you, you can still track their contents and see meaningful header diffs in git. – Chris Stratton Dec 12 '19 at 20:00