3

I'm trying to test whether a member of a struct that is declared as a pointer to void was defined before calling a function. However, it seems that whenever I add anything extra to the bare bones program (such as the std::cout at the end), 'testB' always equates to true. I can't change how 'struct a' is declared because it is part of an external library.

So, how do I test that a member of a struct that is declared as a void pointer is in fact defined with the proper type?

Sample structs

struct b { };

struct a {
   void *b;
};

Sample code I am trying to test

struct a myA;
struct b myB;
myA.b = &myB;
foo(myA);

Sample test program that is failing

#include <iostream>

int main() {

   struct a aa;
   struct b bb;
   // the following line is commented out so I'm expecting 'testB' to evaluate to false
   // aa.b = &bb;

   struct b *testB = static_cast<struct b*>(aa.b);

   if(testB) {
      std::cout << "is defined" << std::endl;
   } else {
      std::cout << "is not defined" << std::endl;
   }

   std::cout << "why does adding this line out result in testB equating to true" << std::endl;

   return 0;
}
surtyaar
  • 2,532
  • 1
  • 18
  • 13
  • Use the test framework. – Ron Apr 17 '18 at 14:53
  • 2
    I do not see you initializing `aa.b` in your test code. Reading an uninitialized value is undefined behaviour. – Quentin Apr 17 '18 at 14:58
  • Are you looking to see if object `aa` has declared (defined) a member variable named `b`, of if the member variable `aa.b` has been initialized to a non-NULL value? – 1201ProgramAlarm Apr 17 '18 at 15:01
  • Thanks @Quentin you are right - in this case my test should fail. Updating question to include a commented out line for better explanation. – surtyaar Apr 17 '18 at 15:03
  • 1
    My comment stands -- not initializing `aa.b` then trying to test it triggers undefined behaviour. This can, among other things, produce different results every time you try. – Quentin Apr 17 '18 at 15:06
  • @Quentin thats what I'm trying to test. Otherwise my program runs into a segfault in production which is what I am trying to avoid. – surtyaar Apr 17 '18 at 15:08
  • 2
    If you can't change how `a` is defined, then you're toasted. There is no way to check whether a variable is initialized: ensuring that no one accesses a variable before it is initialized is the developer's job. – Quentin Apr 17 '18 at 15:10
  • @1201ProgramAlarm I believe I am trying to test for the latter. By 'declared' I mean that the struct's "schema" was declared to have a member 'b' that is a void pointer. By 'defined' I am trying to say that the "a's" member b has been set with an instantiated object with type 'struct b'. – surtyaar Apr 17 '18 at 15:11
  • The latter can be satisfied by `dynamic_cast` if, [and only if](https://stackoverflow.com/questions/4227328/faq-why-does-dynamic-cast-only-work-if-a-class-has-at-least-1-virtual-method), the `struct` is polymorphic & pointer is either `nullptr` or to a live instance of that or another dynamic type. Otherwise, again, the language assumes you did the right thing & doesn't burden itself with ways to prove you did: a `void*` is a `void*`, & if you want to convert it to anything else, better be sure you actually instantiated that thing there. The answer in real code is usually to stop using `void*` – underscore_d Apr 17 '18 at 15:21

3 Answers3

6

This is an interesting question because you are asking two questions:

  • Does some type T have a member named b?
  • If the previous was true, is that member of type void*?

We can do all this at compile time.

In your solution, you should not try to perform your static casting as you run risks of violating aliasing rules, and you also make the assumption that the static member appears first.

We can use a simplistic detection idiom to detect whether the condition is satisfied:

template<class T, class=void>
struct has_void_ptr_member_named_b_t : std::false_type{};

template<class T>
struct has_void_ptr_member_named_b_t<T, std::void_t<decltype(std::declval<T>().b)>> : 
    std::is_same<std::decay_t<decltype(std::declval<T>().b)>, void*>
{};
template<class T>
constexpr bool has_void_ptr_member_named_b_v = has_void_ptr_member_named_b_t<T>::value;

And now you can ask has_void_ptr_member_named_b_v which can be converted to bool, for any given type T

For example:

struct b { };
struct a {
   void *b;
};
// ...

std::cout << std::boolalpha << has_void_ptr_member_named_b_v<a> << std::endl; // true
std::cout << std::boolalpha << has_void_ptr_member_named_b_v<b> << std::endl; // false

Demo

This uses C++17 for its std:void_t, which is pretty easy to create yourself. You can roll all of this in C++11 if you try hard enough, I think. You can also look into std::experimental::is_detected if you wish to use a pre-made detection idiom (that's currently nonstandard).


As @YSC pointed out in the comments below, perhaps you only mean to test whether aa.b is non-null?

You should initialize it to nullptr because, as a fundamental type (pointer) it has no default constructor, and remains uninitialized:

struct a {
   void *b = nullptr;
};

You should still avoid your casting because you run the risk of undefined behavior, and you're assuming that the structs always line up.

a aa;
b bb;
auto test = [&aa](){
    if (aa.b)
        std::cout << "aa.b is defined\n";
    else
        std::cout << "aa.b is null\n";
};
test(); // "aa.b is null"
aa.b = &bb;
test(); // "aa.b is defined"

Demo 2

AndyG
  • 39,700
  • 8
  • 109
  • 143
  • Andy, I'm not sure this answers the question. OP is more interested it seems to the runtime information, is that (`void*`) pointer pointing to a valid object? (I'm not sure, OP is unclear) – YSC Apr 17 '18 at 15:15
  • @YSC: Then I misread the OPs intent :-( It seemed to me their later sentences indicated they just wanted to know if the member existed as a void pointer. – AndyG Apr 17 '18 at 15:17
  • 1
    Why are you using conjunction at all? (True and X) is the same as (X). It looks to just complicate it even more. Also, if already requiring c++17, why define your own void_t? – Chris Uzdavinis Apr 17 '18 at 15:17
  • @ChrisUzdavinis: Thanks for pointing all that out. Mixture of stopping after "it works" and considering a C++14 answer vs C++17 answer – AndyG Apr 17 '18 at 15:22
  • I recently had code review rejected for being too complicated, so I got sensitive to trying to make this stuff as easy to read (so to speak) as possible, and this hits close to home. :) – Chris Uzdavinis Apr 17 '18 at 15:24
  • Neat and elegant. – Lingxi Apr 17 '18 at 15:28
  • Suggest replacing `std::void_t` with a void type-cast. – Lingxi Apr 17 '18 at 15:28
  • @YSC: I think you may be right... updated post to address both interpretations. – AndyG Apr 17 '18 at 15:29
  • @Lingxi: `void_t` is required to create a non-deduced context so that SFINAE works properly. – AndyG Apr 17 '18 at 15:53
  • @AndyG Something like [this](https://wandbox.org/permlink/L2w5GvvdbdzgovQf). – Lingxi Apr 17 '18 at 15:56
  • @Lingxi: Ah, so that's what you meant. Thank you. I still prefer void_t :-) – AndyG Apr 17 '18 at 15:58
  • Not sure if I am missing something but 'Demo 2' seems to be dependent on architecture. I tried compiling/running on osx and I get "aa.b is defined" printed twice. Regardless.. thanks for your answer - learned a lot! – surtyaar Apr 17 '18 at 20:31
  • @surtyaar: Looks like I linked the wrong code for Demo 2. I've updated the link. Try that! – AndyG Apr 17 '18 at 22:57
1

You need to ensure that aa.b is initialised before you try to use it's value. The easiest way to ensure that is to change a so that it initialises in the constructor.

Here are some ways to do that

struct a
{
    void* b = nullptr; // data member with initialiser
}

struct a
{
    void* b;
    a() : b(nullptr) {} // user defined default constructor
}

struct a
{
    void* b;
    a(void* _b = nullptr) : b(_b) {} // user defined constructor with default parameter
}
Caleth
  • 52,200
  • 2
  • 44
  • 75
1

If you cannot modify A, you must take measures into your own hands to ensure it is initialized. Perhaps a Wrapper (Adapter) object can be used as a better implementation of A. Here, it is default initialized in the class body, so you can be sure the pointer will be null if it's not initialized. You can use a type-safe wrapper like SafeA below in place of "struct a" in all your code, and then get_a() when you need to interact with the library that defined it. (A really awful interface, btw, but you're stuck.)

I'm not dealing with memory management of the object the b* points to, that's your problem. :)

class SafeA {
private: 
    a aa{}; // will initialize internal pointer in aa to nullptr

public:
    SafeA() = default;
    SafeA(SafeA const &) = default;
    SafeA(b * bPtr) : aa{bPtr} { }
    SafeA& operator=(SafeA const&) = default;
    SafeA& operator=(b *ptr) {
       aa.b = ptr;
       return *this;
    }       
    a& get_a() { return aa; }
    a const& get_a() const { return aa; }
};

If you have a void*, and it is initialized to nullptr, you can tell if it's null or not-null, but you cannot determine if it is "properly" initialized to point to the right type. That's entirely a problem the author of that interface forced upon you. If you do not initialize a pointer when it's declared, you cannot later determine if it contains a valid address or garbage, so you want to always initialize it, and you cannot determine the type of thing it points to. So be careful and use wrappers to help prevent untyped access to it.

Chris Uzdavinis
  • 6,022
  • 9
  • 16