C++17 is amazing

I just wrote some code that blew my mind and wanted to share: https://godbolt.org/g/9BN4LE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <memory>

void func(int *p)
{
	++(*p);
}

template<typename R, typename Arg>
struct Helper final
{
    using Arg_t = Arg;
    Helper(R (&)(Arg)) noexcept
    {
    }
    Helper(R (*)(Arg)) noexcept
    {
    }
};

template<auto Func, typename T = typename decltype(Helper(Func))::Arg_t>
struct Caller final
{
    using pointer = T;
	void operator()(pointer p) const
	{
		Func(reinterpret_cast<typename decltype(Helper(Func))::Arg_t>(p));
	}
};

int main()
{
    int var = 1234567890;
    {
	    std::unique_ptr<int, Caller<func>> ptr {&var};
    }
    return var;
}
Function pointers/references have always been able to be arguments to templates, but before C++17 you'd always have to know the return type, calling convention, and argument types in advance. Now, with template<auto> and class template parameter deduction, we can have arbitrary functions as template parameters without needing to specify any information about the function.

The use case for this specific code is wrapping Windows API handles - for example, the type std::unique_ptr<HMENU, Caller<DestroyMenu>> would wrap a menu handle. The member type alias pointer is used by std::unique_ptr to know what the actual pointer type is (so you don't end up with HMENU *). The reinterpret_cast is because sometimes the cleanup function Windows wants you to use takes a different argument type than the actual handle type. Using std::unique_ptr works great because you only ever need to use .get(), .release(), or .reset() - it's a lot easier than writing our own handle wrapper, so instead it can just be a template alias:
1
2
3
template<typename Handle, auto FreeFunc>
using unique_handle = std::unique_ptr<Handle, Caller<FreeFunc, Handle>>;
using menu_handle = unique_handle<HMENU, DestroyMenu>;


Previously doing this kind of abstraction would've required a lot more template parameters to be passed (return type, argument type, etc), or passing the cleanup function as a parameter to a function and storing it as a member variable or using type erasure, etc. but now it's as simple as just passing the function as a template parameter and letting the rest be deduced automatically. In the past I would use std::unique_ptr's support for having a function pointer/reference as the deleter, but then you'd have to remember to always pass the cleanup function to the constructor. Making the cleanup function part of the type is much more convenient.

The best part is that it's a zero-overhead abstraction, so the compiler can completely optimize it as shown in the godbolt demo link.

Just felt like sharing that. Also std::variant is a life saver for one of my projects, along with if constexpr. C++17 is awesome.
Last edited on
The reinterpret_cast is because sometimes the cleanup function Windows wants you to use takes a different argument type than the actual handle type.
Huh? For example?
@helios Bitmap handles are HBITMAP but to free them you must call DeleteObject which takes as its parameter an HGDIOBJ. There are other such cases but this is the one I most recently came across.
Those are both typedefs for HANDLE, though. Documented: https://msdn.microsoft.com/en-us/library/windows/desktop/aa383751
That documentation appears to be severely outdated. In practice (if you actually examine the types), HBITMAP is an opaque pointer created with DECLARE_HANDLE such that it is effectively a unique type in the type system different from all other handle types. HGDIOBJ is a void *.

Here's windef.h:
72
73
74
75
#if !defined(_MAC) || !defined(GDI_INTERNAL)
DECLARE_HANDLE(HBITMAP);
DECLARE_HANDLE(HBRUSH);
#endif 
Here's winnt.h:
632
633
634
635
636
637
638
639
640
641
642
643
#ifdef STRICT
typedef void *HANDLE;
#if 0 && (_MSC_VER > 1000)
#define DECLARE_HANDLE(name) struct name##__; typedef struct name##__ *name
#else
#define DECLARE_HANDLE(name) struct name##__{int unused;}; typedef struct name##__ *name
#endif
#else
typedef PVOID HANDLE;
#define DECLARE_HANDLE(name) typedef HANDLE name
#endif
typedef HANDLE *PHANDLE;
STRICT is defined. The end result is that HBITMAP is of type HBITMAP__ *, not HANDLE.

EDIT: Another example, FormatMessage requries that you call LocalFree on the TCHAR * buffer it allocates, but LocalFree takes as its parameter an HLOCAL.
Last edited on
Sure, but all that matters is that the function accepts void *.
My point is that I think it's better to leave the cast out and handle some cases specially than to cast everything and miss out on some protection that the compiler might have been able to provide.
Ah, I see your point. In that case I'll just add a boolean template parameter to force casting and default it to false, and use if constexpr to choose the relevant call to Func. Thanks!
Topic archived. No new replies allowed.