42

I'm using an API that accepts void* in certain functions. I frequently accidentally pass the wrong pointer type to the function, and of course it compiles fine, but doesn't work at runtime.

Is there a way to disable implicit conversion to void* for pointers to a certain class?

Peter - Reinstate Monica
  • 15,048
  • 4
  • 37
  • 62
AILien
  • 832
  • 4
  • 11
  • 30
    "*Is there any way to disable implicit cast to `void*` for pointers to a certain class?*" - No, there is not. All pointers are implicitly convertible to `void*`, that is a core feature of the C++ language. I would probably wrap the API function in question and make the wrapper only accept pointers of the correct type, and then pass them along to the API as needed. – Remy Lebeau Aug 05 '21 at 21:02
  • 1
    Is it your own API? Can you make changes to it? – Ted Lyngmo Aug 05 '21 at 21:03
  • 1
    https://softwareengineering.stackexchange.com/questions/275712/why-arent-void-s-implicitly-cast-in-c – SamR Aug 05 '21 at 21:03
  • 5
    @SamR that question is about casting `void*` to other pointer types. That is the opposite of this question – Remy Lebeau Aug 05 '21 at 21:04
  • 9
    This question is about an **implicit conversion**. There is no such thing as an implicit cast. A cast is something you write in your source code to tell the compiler to do a conversion. – Pete Becker Aug 05 '21 at 21:06
  • 11
    One thing you can do is wrap the API with your own functions that make passing an invalid pointer impossible. – user4581301 Aug 05 '21 at 21:07
  • 1
    [This is](https://godbolt.org/z/reTPre5YG) the best I could come up with. It works, but require your api to be templated. You can hide your original API from end user and expose these wrappers instead. – C.M. Aug 05 '21 at 21:41
  • 1
    Wrap the functions. If you've got a legacy API, then the answer is to wrap it and not touch it directly. – Silvio Mayolo Aug 06 '21 at 15:16
  • 2
    I don't understand your problem. Are you passing pointers to *objects of the wrong type* -- something only detected at run time, and then usually disastrous? How would making the conversion mandatorily explicit instead of allowing implicit conversions prevent that? Also, because a typesafe wrapper API would trivially fix that, I think you mean something else. – Peter - Reinstate Monica Aug 07 '21 at 10:23
  • 3
    As always, example code would remove all misunderstandings. – Peter - Reinstate Monica Aug 07 '21 at 10:26
  • 2
    This seems like an X-Y problem. Any object pointer is implicitly convertible to type `void *`, *and the result of such a conversion points to the same object as the original* -- the pointer type just doesn't carry information about the type of the pointed-to object. If there is an API designed to work with void pointers in the first place then it cannot be the conversion itself that is the problem. – John Bollinger Aug 08 '21 at 14:22
  • 1
    It's not unusual for an API to accept void pointers that will be later passed back to a user provided call back function. – AILien Aug 09 '21 at 08:56

4 Answers4

39

Is there any way to disable implicit conversion to void* for pointers to a certain class?

No, you can't prevent the implicit conversion, but you could wrap the API function(s) in proxy functions that checks types at compile time and approve/disapprove them there.

Example:

#include <iostream>
#include <string>
#include <type_traits>

void api(void* p) { // your original API
    std::cout << "void* " << p << '\n';
}

template<class T>
void api_wrapper(T* p) { // your wrapper
    // let constness fail in the original api instead of in the static_assert:
    using type = std::remove_const_t<T>*;

    static_assert(
        // add your approved types here
        std::is_convertible_v<type, std::ostream*> ||
        std::is_convertible_v<type, std::string*>,
        "Not an approved type"
    );
    api(p);
}

int main() {
    std::string foo;
    api_wrapper(&std::cout);
    api_wrapper(&foo);
    //api_wrapper(&std::cin); // compile time error "Not an approved type"
}

If the set of pointer types that you would like to disapprove is very small, then instead of listing all the approved types in the static_assert, just list the disapproved types and adjust the boolean logic:

    static_assert(
        // add your disapproved types here
        not std::is_convertible_v<type, std::ostream*> &&
        not std::is_convertible_v<type, std::string*>,
        "Not an approved type"
    );
Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
  • 2
    Caution: a pointer whose conversion to (here) `std::string *` requires an adjustment will pass your `static_assert`, but be converted to `void *` without the adjustment, which will certainly fail down the line. – Quentin Aug 06 '21 at 07:21
  • @Quentin I don't like the sound of that, but I don't fully understand what you are saying. Would you mind using [this example](https://godbolt.org/z/99x1xYMPK) to illustrate it? – Ted Lyngmo Aug 06 '21 at 07:35
  • I think that says if you can do something with type Y (std::string *) that you cannot do with type X (yourtype), and you can convert X to Y, but the api converts type X to type Z (void *), you do not get the functionality of type Y. – Sumurai8 Aug 06 '21 at 07:44
  • 1
    @TedLyngmo sure, [here you go](https://godbolt.org/z/nWEe7P3a3). – Quentin Aug 06 '21 at 07:49
  • @Quentin Ok, I think I understand, but the original API didn't do these adjustments either. One could of course call `api` with an adjusted pointer from within `api_wrapper` or use `is_same_v` in the `static_assert` to force it upon the user to provide the exact types. – Ted Lyngmo Aug 06 '21 at 07:59
  • 4
    To be fair the question is somewhat underspecified, in particular I don't see how the API itself could distinguish different types even if it expects them. I suspect each of the API's functions actually expects a single known type. – Quentin Aug 06 '21 at 08:04
  • someone actually wrote the word `not` instead of `!` ? – user253751 Aug 06 '21 at 14:22
  • @user253751 I often do because when my eyes get tired I often miss the little `!` :-) – Ted Lyngmo Aug 06 '21 at 14:28
37

You can add a deleted overload for the API function:

// API function
void api(void* p) {
    // ... 
}

// Special case for nullptr
inline void api(std::nullptr_t) {
    api((void*)nullptr);
}

// Everything else is disabled
template <class ...T>
void api(T&&... t) = delete;

int main() {
    int i = 0;
    void* p = &i;
    api(p); // Compiles
    // api(&i); // Doesn't compile
}
Aykhan Hagverdili
  • 28,141
  • 6
  • 41
  • 93
  • 2
    With this addition, all calls will probably look like `api(static_cast(&i));` instead of `api(&i);`.and if OP accidentally pass an object of the wrong type to the function, how will this help? – Ted Lyngmo Aug 06 '21 at 08:19
  • 5
    @TedLyngmo as I understand, OP wants to only pass `void*` and disable all other types. I don't think OP has a set of approved types. I might be wrong though. – Aykhan Hagverdili Aug 06 '21 at 08:25
  • 3
    I see what you mean, and if you are correct, this answer should be spot on! – Ted Lyngmo Aug 06 '21 at 09:06
  • 5
    This and @TedLyngmo's answers are both very good. Marking this as correct though because I found it the most enlightening. As it happens there was only really one type I ever wanted to pass to the API and only one another that I was accidentally passing. So I could do a non-template version of this to just block the wrong type from being passed. However, I discovered that the API, although not in my control, will respond to a macro definition to allow me to choose a specific type rather than void* as the parameter to these funcs. So I'm doing that instead. Very interesting answers though! – AILien Aug 06 '21 at 15:12
21

From the standard, expr.conv.ptr:

A prvalue of type “pointer to cv T”, where T is an object type, can be converted to a prvalue of type “pointer to cv void”. The pointer value (basic.compound) is unchanged by this conversion.

You cannot disallow this conversion.

Nathan Pierson
  • 5,461
  • 1
  • 12
  • 30
4

If the argument is meant to be opaque to the client, you might expose it as a handle type that does not implicitly convert, e.g.

#include <cstdint>

using api_handle = std::uintptr_t;

inline api_handle make_api_handle(const char* p)
{
  return (api_handle)(const void*)p;
}

inline api_handle make_api_handle(const int* p)
{
  return (api_handle)(const void*)p;
}

The two-stage conversion is because the language standard technically only says that round-trip conversions between any object pointer and void* are safe, and that round-trip conversions between void* and uintptr_t or intptr_t are safe. Within your library, you can do the conversion the opposite way to retrieve the original pointer.

That somewhat-contrived example lets you convert specific types of pointers to handles explicitly, but pointers do not convert to handles implicitly. (Although, now, integral values will implicitly convert to handles, and give you undefined behavior.) In the real world, the helper functions should be optimized out.

If this kind of approach works for your API, another solution would be to wrap your void* in a minimal struct and pass those around. Modern compilers should be able to pass them around in registers, just like pointers. You might also add a field storing type or other information. Another option might be to keep a vector of valid objects and pass around indices to that, like file handles in Unix.

For constructors, you can use the explicit keyword to disable all implicit conversion on the arguments.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Davislor
  • 14,674
  • 2
  • 34
  • 49