1

I'm trying to create a template where a default value can be passed as a non-type parameter.

The original type (WINAPI's HANDLE) is a pointer type from the compiler perspective, but otherwise is treated as an integral type from a user perspective.

// Somewhere in system headers
#define INVALID_HANDLE_VALUE ((HANDLE)(LONG_PTR)-1)
typedef void *HANDLE;

// My code
template<typename Handle, Handle Default>
class HandleWrapper
{
    ...
};

HandleWrapper<HANDLE, INVALID_HANDLE_VALUE>; // error: invalid nontype template argument of type `HANDLE`

My current workaround:

template<typename Handle, uintptr_t Default>
class HandleWrapper
{
    static_assert(std::is_pointer<Handle>::value, "Handle must be a pointer");
    static constexpr Handle DefaultHandle = reinterpret_cast<Handle>(Invalid);
};

I guess the right solution is to somehow specify that typename Handle should be treated as an integral type (uintptr_t) as long as conversation is not narrowing.

Kentzo
  • 3,881
  • 29
  • 54
  • 2
    Does your example compile? A reinterpret cast is not a constant expression. – Kerrek SB Mar 02 '18 at 23:51
  • I don't really see why you need a template here. –  Mar 02 '18 at 23:51
  • @KerrekSB `reinterpret_cast` is a compiler directive unlike `static_cast`. – Kentzo Mar 03 '18 at 00:05
  • @NeilButterworth Template is needed because different handles may have different default values, it's not always -1. – Kentzo Mar 03 '18 at 00:07
  • But presumably it is always an integer? In which case I don't see why a non-templated constructor cannot be used. –  Mar 03 '18 at 00:10
  • I would like to avoid repeating the default value each time object is created by e.g. `using ProcessWrapper = HandleWrapper`. – Kentzo Mar 03 '18 at 00:18
  • Use default constructor parameter? Your code seems to be a lot of effort for minimal gain. And of course it doesn't work. –  Mar 03 '18 at 00:20
  • How a default constructor can be used if default argument is different for each Handle type? Do you suggest overloading default constructor for each Handle type? – Kentzo Mar 03 '18 at 00:26
  • Not all Win32 handle types are pointers. `SOCKET`, for example, is a `UINT_PTR` (a pointer-sized `UINT`, similar to `uintptr_t`). Also be careful, because if `STRICT` is defined, all actual pointer-based Win32 handle types DO NOT map to `void*` (`HANDLE` is always `void*`, though), they are pointers to structs instead. In my own handle-wrapper class, I define a `traits` class for each handle type to define the default value, closure function, comparisons, etc, and then use a template parameter in the wrapper class to specify which `traits` class to use. – Remy Lebeau Mar 03 '18 at 01:48

1 Answers1

1

In my code, I have a templated handle wrapper class that I use, but I take a different approach with it than you are with yours. I define a separate traits class for each type of handle to be wrapped, and then use a template parameter to specify which traits to use in the wrapper.

Try doing something similar in your case, eg:

struct InvalidHandleTraits
{
    using HandleType = HANDLE;
    static constexpr HANDLE DefaultHandle = INVALID_HANDLE_VALUE;
    //...
    static void Close(HANDLE h) { CloseHandle(h); }
    //...
};

struct NullHandleTraits
{
    using HandleType = HANDLE;
    static constexpr HANDLE DefaultHandle = NULL;
    //...
    static void Close(HANDLE h) { CloseHandle(h); }
    //...
};

... other traits as needed...

template<typename traits = InvalidHandleTraits>
class HandleWrapper
{
public:
    using HandleType = typename traits::HandleType;

    HandleWrapper(HandleType h = traits::DefaultHandle) : m_handle(h) { std::cout << "constructor: " << h << std::endl; }
    ~HandleWrapper() { traits::Close(m_handle); }
    //...

    operator HandleType() { return m_handle; }

private:
    HandleType m_handle;
};

Then you can use the desired traits class when needed, eg:

HandleWrapper<> h = CreateFile(...);
// or:
// HandleWrapper<InvalidHandleTraits> h = CreateFile(...);

HandleWrapper<NullHandleTraits> h = CreateFileMapping(...);

Live Demo

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • I understand that my abstraction is not ideal and the traits-like approach is indeed better. However, I'd like still to know whether there is an utility method or at least a technique that would require a compiler to tread a pointer as if it were integer. – Kentzo Mar 05 '18 at 04:51
  • 1
    @Kentzo a pointer is not an integral type. The only way to make the compiler treat a pointer as an integer is to typecast it – Remy Lebeau Mar 05 '18 at 06:59
  • Technically it can be represented as an integer, hence the existence of `uintptr_t`. I guess it was not taken into considiration by C++ Standard Committee. – Kentzo Mar 05 '18 at 23:56
  • @Kentzo `uintptr_t` is a pointer-sized integer, but it is not itself a pointer. You still need a typecast to put a pointer into a `uintptr_t` – Remy Lebeau Mar 06 '18 at 00:21
  • This is clear. What's is not clear is lack of the compiler directive that would tell the compiler to treat pointer as an integer type `uintptr_t`. This is useful for bridging C-API into C++ where `void*` is often used as an "abstract handle". – Kentzo Mar 06 '18 at 00:26
  • This code doesn't compile with modern compilers. The `static constexpr HANDLE DefaultHandle = INVALID_HANDLE_VALUE;` line fails due to the embedded cast within the `INVALID_HANDLE_VALUE` macro. The cast is equivalent to a `reinterpret_cast`, which makes the value not a `constexpr`. Checking on [Compiler Explorer](https://godbolt.org/), the above compiled with GCC 6.4, but fails for GCC 7.1 and onward. It also fails on Clang (all versions). – Daniel Stevens Nov 04 '19 at 19:48
  • @DanielStevens the `INVALID_HANDLE_VALUE` macro is copied directly from the Win32 API, it really does use a C-style cast to convert `-1` to `void*`. The code works fine for me in the [Live Demo](https://ideone.com/ThEZTN) I linked to in my answer, which is using GCC 8.3. – Remy Lebeau Nov 04 '19 at 21:26
  • Yes, the standard Windows definition of that macro does contain a cast. That's the problem. A `reinterpret_cast` is specifically restricted from use with `constexpr`. Reference [Constant expressions](https://en.cppreference.com/w/cpp/language/constant_expression), point 16. Equivalent C-style casts are also restricted. A template non-type parameter must be `constexpr`. Hence a conforming compiler should reject code that uses `INVALID_HANDLE_VALUE` as a template non-type parameter. Older versions of GCC did not conform. I noticed ideone listed GCC 8.3. I can't explain that. It fails on Godbolt. – Daniel Stevens Nov 05 '19 at 05:41
  • 1
    @DanielStevens apparently a known gcc bug: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=49171, but says it was fixed in 8.1. Also see https://stackoverflow.com/questions/24398102/ – Remy Lebeau Nov 05 '19 at 15:35
  • For what it's worth, I was able to work around this by using a `uintptr_t` value for the template parameter instead of a `HANDLE` value. Internally I could cast the default value to a `HANDLE` type. Of course that means instantiating the template with a literal `-1` or `0` instead of `INVALID_HANDLE_VALUE` or `NULL`. – Daniel Stevens Nov 06 '19 at 04:41
  • 1
    @DanielStevens in my case, for older compilers, I leave the template parameter as the actually type and just make the `DefaultHandle` be a `uintptr_t` that I typecast in assignments and comparisons. – Remy Lebeau Nov 06 '19 at 07:03
  • 1
    An alternate workaround I found is to use a function in the traits struct: `static HANDLE NullValue() { return INVALID_HANDLE_VALUE; }`. A function in a template struct does not need to be `constexpr`, unlike a static data declaration. If optimizations are turned on to even the lowest level, the function call will be inlined, producing results consistent with a `static constexpr` data member. And no casting required. – Daniel Stevens Nov 12 '19 at 05:44