4

I have the following typedefs in my code:

#define FOO_OFF 0
#define FOO_ON 1
typedef uint8_t foo;

#define BAR_NO  0
#define BAR_YES 1
#define BAR_UNKNOWN 255
typedef uint8_t bar;

Those two types, although they have the same underlying type, they do not carry the same information.

And actually, I would like to get a warning if anyone in the code does something like:

foo foovar = FOO_OFF;


void get_bar(bar *outvar)
{
    // assigning the bar variable a foo variable content
    *outvar = foovar;
}

I could not find any such warning option in gcc, I have come accross -Wconversion but this warns only if there is a chance of losing information which is not the case in my example.

Does anyone know if there is something I can do? Obviously, it should be possible to cast when a type change is really needed.

Louis Caron
  • 1,043
  • 1
  • 11
  • 17
  • 1
    For the sake of the question you could have given those types more distinguishable names, like `foo` and `bar` – Jorengarenar Jul 16 '20 at 14:39
  • 1
    Since typedef is just an alias, I would be surprised if there would be an option for this. The compiler only needs to resolve the alias and not care of the name used after that. – Sami Kuhmonen Jul 16 '20 at 14:39
  • 2
    Search for _strong typedef_. It has been discussed many times over. But basically, you want to wrap your basic types in `struct`s that convey their meaning and can be used to ensure type safety. Also, please just return values instead of setting outputs via argument pointers. – underscore_d Jul 16 '20 at 14:40
  • So `rolloverdetection detection` may only be set by the function and not as a statement. Then only export the function, not the variables. – Paul Ogilvie Jul 16 '20 at 14:43
  • that is one of the really good things in c. typedef did NOT define a type. Instead it creates an alias. So you should use the "using" keyword instead which makes this clearer. :-) To achieve what you want you have to create a type by using a struct which holds your underlying type. – Klaus Jul 16 '20 at 14:51
  • use larger type for `detect` https://godbolt.org/z/Y54536 – 0___________ Jul 16 '20 at 14:58

3 Answers3

3

Short answer is no, you can't do this. typedef declare an alias, not a new type, so any compiler respecting the standard cannot have the feature you want right now.

However, you can achieve it, by introducing a new type, using an enum, or a struct.

If you're in C, you will be able to cast from one enum to the other easily.

Because the address of the first element of a struct is also the address of a struct, you would be able to cast it from and to int8 or another struct, by casting the struct address, then de-referencing the pointer with it's new type. (*((dest_type *)&value))

Phantomas
  • 206
  • 1
  • 6
  • 1
    I don't understand what you mean about structs. You can't cast one struct to another, even if they have identical definitions. `aa = (struct a)bb` does not work. Are you suggesting something like `aa = *(struct a *)&bb`? – Nate Eldredge Jul 16 '20 at 16:24
  • Oh right yes, that what I meant, I should have precised that you can do it by casting the address, not the structure itself. I'll edit my answer – Phantomas Jul 16 '20 at 16:38
0

I quite like the solution the team at work of the author of question Strongly typed using and typedef has come up with, as it's really minimal and easy:

#define STRONG_TYPEDEF(T, D)                                 \
    class D : public T {                                     \
    public:                                                  \
        template<class... A> explicit D(A... a) : T(a...) {} \
        const T& raw() const { return *this; }               \
    }

Unfortunately it won't work with primitive types. But it's similar to BOOST_STRONG_TYPEDEF, which will do.

Here is a little example and vestigial "comparison" of both:

#include <iostream>

#define USE_BOOST true

#if USE_BOOST

#include <boost/serialization/strong_typedef.hpp>
BOOST_STRONG_TYPEDEF(std::string, foo);
BOOST_STRONG_TYPEDEF(std::string, bar);

BOOST_STRONG_TYPEDEF(int, myInt); // Boost allows primitives

#else

#define STRONG_TYPEDEF(T, D)                                 \
    class D : public T {                                     \
    public:                                                  \
        template<class... A> explicit D(A... a) : T(a...) {} \
        const T& raw() const { return *this; }               \
    }

STRONG_TYPEDEF(std::string, foo);
STRONG_TYPEDEF(std::string, bar);

// STRONG_TYPEDEF(int, myInt); // this one just classes

#endif

int main()
{
    std::string a = "abc";
    foo b{"abc"};
    bar c = static_cast<bar>(a);
    std::string d = c; // we can assign back to base type

#if USE_BOOST // Boost
    myInt x{4};  // only allows this type of initialization
    switch (x) { // allows primitives, so `switch` works
        case 1:
            std::cout << 1 << std::endl;
            break;
        case 4:
            std::cout << 4 << std::endl;
            break;
    }
#else // Boost don't allow the following:
    if (b == c) {                    // comparing
        std::cout << c << std::endl; // printing
    }
#endif

    /* But we can't
       foo e = a; // assign base type to new type
       foo f = c; // assign one type to another
    */

    return 0;
}
Jorengarenar
  • 2,705
  • 5
  • 23
  • 60
0

If the size doesn't have to be one byte, we can (ab)use pointers:

typedef struct foo *rolloverdetection;
typedef struct bar *rolloverdetected;

#define ROLLOVERDETECTION_OFF    ((rolloverdetection) 0)
#define ROLLOVERDETECTION_ON     ((rolloverdetection) 1)

#define ROLLOVERDETECTED_NO      ((rolloverdetected) 0)
#define ROLLOVERDETECTED_YES     ((rolloverdetected) 1)
#define ROLLOVERDETECTED_UNKNOWN ((rolloverdetected) 2)

The preprocessor symbols aren't constant expressions any more and we can't use them as switch labels and whatnot.

A good solution to this is to use C++ type-safe enums. This is one of the advantages of writing your code in "Clean C": an informal name given to working in a language dialect which compiles as some version of C, as well as some version of C++ with the same behavior.

Simply:

typedef enum {
  ROLLOVERDETECTION_OFF,
  ROLLOVERDETECTION_ON
} rolloverdetection;

typedef enum {
  ROLLOVERDETECTED_NO,
  ROLLOVERDETECTED_YES,
  ROLLOVERDETECTED_UNKNOWN = 255
} rolloverdetected;

In C, you can still assign ROLLOVERDETECTED_YES to a variable of type rolloverdetection, but not so in C++.

If you keep the code compiling as C++, you can use a C++ compiler to check for these violations, even if the shipping build of the code doesn't use C++.

If storing the value in 8 bits is important, I seem to recall GCC supports enum-typed bitfields as an extension (not in ISO C):

struct whatever {
  rolloverdetected roll_detect : 8;
};

C++ enums are not perfectly type safe; implicit conversion from an enum member to an integer type is possible:

int roll = ROLLOVERDETECTION_ON;

but not in the reverse direction.

By the way, other techniques open up if you compile as C and C++, like being able to use more nuanced type casts:

#ifdef __cplusplus
#define strip_qual(TYPE, EXPR) (const_cast<TYPE>(EXPR))
#define convert(TYPE, EXPR) (static_cast<TYPE>(EXPR))
#define coerce(TYPE, EXPR) (reinterpret_cast<TYPE>(EXPR))
#else
#define strip_qual(TYPE, EXPR) ((TYPE) (EXPR))
#define convert(TYPE, EXPR) ((TYPE) (EXPR))
#define coerce(TYPE, EXPR) ((TYPE) (EXPR))
#endif

So now, for instance, we can do

strip_qual(char *, str)

In C, this is just an unsafe (char *) str cast. In C++, the above macros produce const_cast<char *>(str). So if str starts out being const char *, but then someone changes it to const wchar_t *, the C++ compiler will diagnose the above cast. Yet, our project doesn't require a C++ compiler in order to build.

In conjunction with this, if you're using GCC, its C++ front end has -Wold-style-cast that will find all the places in the code where you're using the (type) value cast notation.

Kaz
  • 55,781
  • 9
  • 100
  • 149