{"id":112611,"date":"2026-08-13T07:00:00","date_gmt":"2026-08-13T14:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/oldnewthing\/?p=112611"},"modified":"2026-08-13T23:02:59","modified_gmt":"2026-08-14T06:02:59","slug":"20260813-00","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/oldnewthing\/20260813-00\/?p=112611","title":{"rendered":"A little helper class for managing <CODE>LPPROC_<WBR>THREAD_<WBR>ATTRIBUTE_<WBR>LIST<\/CODE>s"},"content":{"rendered":"<p>The <code>LPPROC_<wbr \/>THREAD_<wbr \/>ATTRIBUTE_<wbr \/>LIST<\/code> is a bit annoying to manage. You have to allocate memory for it yourself, but you don&#8217;t know how much; you have to ask <code>Initialize\u00adProc\u00adThread\u00adAttribute\u00adList<\/code>. And then when you&#8217;re done, you have to call <code>Delete\u00adProc\u00adThread\u00adAttribute\u00adList<\/code> before freeing the memory.<\/p>\n<p>We suffered through this when <a title=\"Programmatically controlling which handles are inherited by new processes in Win32\" href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20111216-00\/?p=8873\"> we controlled which handles are inherited by a new process<\/a>. <a title=\"Another way to create a process with attributes, maybe worse maybe better\" href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20130426-00\/?p=4543\"> I wrote a helper function<\/a> to try to make it easier, by taking the attributes as a separate parameter beyond the parameters to <code>Create\u00adProcess<\/code> but I&#8217;m not sure if it was entirely successful.<\/p>\n<p>Here&#8217;s another try, this time building on the Windows Implementation Library.<\/p>\n<pre>namespace details\r\n{\r\n    inline void FreeProcThreadAttributeList(\r\n        _Pre_valid_ _Frees_ptr_ LPPROC_THREAD_ATTRIBUTE_LIST list)\r\n    {\r\n        ::DeleteProcThreadAttributeList(list);\r\n        ::HeapFree(::GetProcessHeap(), 0, list);\r\n    }\r\n};\r\n\r\nusing unique_proc_thread_attribute_list = wil::unique_any&lt;LPPROC_THREAD_ATTRIBUTE_LIST,\r\n    decltype(&amp;details::FreeProcThreadAttributeList), details::FreeProcThreadAttributeList&gt;;\r\n\r\nHRESULT make_proc_thread_attribute_list_nothrow(\r\n    DWORD attributeCount, _Out_ LPPROC_THREAD_ATTRIBUTE_LIST* result)\r\n{\r\n    *result = nullptr;\r\n    SIZE_T size = 0;\r\n    InitializeProcThreadAttributeList(nullptr, attributeCount, 0, &amp;size);\r\n    auto p = wil::unique_process_heap_ptr&lt;std::remove_pointer_t&lt;LPPROC_THREAD_ATTRIBUTE_LIST&gt;&gt;(\r\n        static_cast&lt;LPPROC_THREAD_ATTRIBUTE_LIST&gt;(::HeapAlloc(::GetProcessHeap(), 0, size)));\r\n    RETURN_IF_NULL_ALLOC(p);\r\n    RETURN_IF_WIN32_BOOL_FALSE(InitializeProcThreadAttributeList(p.get(), attributeCount, 0, &amp;size));\r\n    *result = p.release();\r\n    return S_OK;\r\n}\r\n\r\nunique_proc_thread_attribute_list make_proc_thread_attribute_list(DWORD attributeCount)\r\n{\r\n    unique_proc_thread_attribute_list result;\r\n    THROW_IF_FAILED(make_proc_thread_attribute_list_nothrow(attributeCount, result.put()));\r\n    return result;\r\n}\r\n<\/pre>\n<p>We start by declaring a helper function that cleans up an <code>LPPROC_<wbr \/>THREAD_<wbr \/>ATTRIBUTE_<wbr \/>LIST<\/code> by deleting the contents, and then freeing the buffer. We use that to define a <code>unique_<wbr \/>proc_<wbr \/>thread_<wbr \/>attribute_<wbr \/>list<\/code> which holds a heap-allocated pointer that has been initialized as a <code>LPPROC_<wbr \/>THREAD_<wbr \/>ATTRIBUTE_<wbr \/>LIST<\/code>.<\/p>\n<p>The first helper function is the nonthrowing version: it asks for the required size of a <code>LPPROC_<wbr \/>THREAD_<wbr \/>ATTRIBUTE_<wbr \/>LIST<\/code> for the specified number of attributes, then allocates that much memory on the heap, storing it in a <code>unique_<wbr \/>process_<wbr \/>heap_<wbr \/>ptr<\/code> so that it will be freed if we fail to initialize it. Declaring that <code>unique_<wbr \/>process_<wbr \/>heap_<wbr \/>ptr<\/code> is a bit of a pain because we want it to be a &#8220;unique pointer to whatever it is that <code>LPPROC_<wbr \/>THREAD_<wbr \/>ATTRIBUTE_<wbr \/>LIST<\/code> points to.&#8221; It&#8217;s also annoying that we have to repeat ourselves in both the template type parameter as well as in the cast of the heap-allocated pointer, because CTAD doesn&#8217;t work here.<\/p>\n<p>After we allocate the memory, we try to initialize it. If that fails (and I can&#8217;t imagine why), we propagate the error, and the RAII type frees the (uninitialized) heap memory.<\/p>\n<p>If initialization succeeds, we return the pointer to the caller, who now takes responsibility for freeing it.<\/p>\n<p>Note that the temporary holding place has to be a <code>unique_<wbr \/>process_<wbr \/>heap_<wbr \/>ptr<\/code> and not a <code>unique_<wbr \/>proc_<wbr \/>thread_<wbr \/>attribute_<wbr \/>list<\/code>: If the initialization fails, we must not call <code>Delete\u00adProc\u00adThread\u00adAttribute\u00adList<\/code>, so we have to hold the heap pointer in something that won&#8217;t try to call <code>Delete\u00adProc\u00adThread\u00adAttribute\u00adList<\/code>.<\/p>\n<p>We can easily use the nonthrowing version to build a throwing version.<\/p>\n<p>My next idea was to let you pass the attributes you want to pre-fill into the attribute list.<\/p>\n<pre>struct proc_thread_attribute {\r\n   template&lt;typename T = void&gt;\r\n   proc_thread_attribute(DWORD_PTR attribute, T* value, SIZE_T size = sizeof(T)) :\r\n      attribute(attribute), value(value), size(size) {\r\n   }\r\n\r\n   DWORD_PTR attribute;\r\n   PVOID value;\r\n   SIZE_T size;\r\n};\r\n\r\ntemplate&lt;typename C&gt;\r\nHRESULT update_proc_thread_attribute_list_nothrow(\r\n    LPPROC_THREAD_ATTRIBUTE_LIST list, C&amp;&amp; attributes)\r\n{\r\n    for (auto&amp;&amp; attribute : attributes) {\r\n        RETURN_IF_WIN32_BOOL_FALSE(\r\n            UpdateProcThreadAttribute(list, 0, attribute.attribute,\r\n                attribute.value, attribute.size, nullptr, nullptr));\r\n    }\r\n    return S_OK;\r\n}\r\n\r\ntemplate&lt;typename C&gt;\r\nvoid update_proc_thread_attribute_list(\r\n    LPPROC_THREAD_ATTRIBUTE_LIST list, C&amp;&amp; attributes)\r\n{\r\n    THROW_IF_FAILED(update_proc_thread_attribute_list_nothrow(\r\n        list, std::forward&lt;C&gt;(attributes)));\r\n}\r\n<\/pre>\n<p>The container parameter can be anything iterable whose value type has <code>attribute<\/code>, <code>value<\/code>, and <code>size<\/code> members. It&#8217;s probably a collection of <code>proc_<wbr \/>thread_<wbr \/>attribute<\/code>s, but it doesn&#8217;t have to be. (Maybe it&#8217;s a collection of things derived from <code>proc_<wbr \/>thread_<wbr \/>attribute<\/code>.)<\/p>\n<p>We can add this to our <code>make_<wbr \/>proc_<wbr \/>thread_<wbr \/>attribute_<wbr \/>list<\/code> function so that callers can pass in a list of attributes they want, and we&#8217;ll make a list that holds them all. And as an extra bonus, you can request room for additional attributes beyond those in the collection you passed in. For example, you might have some attributes that you always use, and then some others you decide on dynamically.<\/p>\n<pre>\/\/ No changes to this function\r\nHRESULT make_proc_thread_attribute_list_nothrow(\r\n    DWORD attributeCount, _Out_ LPPROC_THREAD_ATTRIBUTE_LIST* result)\r\n{\r\n    *result = nullptr;\r\n    SIZE_T size = 0;\r\n    InitializeProcThreadAttributeList(nullptr, attributeCount, 0, &amp;size);\r\n    auto p = wil::unique_process_heap_ptr&lt;std::remove_pointer_t&lt;LPPROC_THREAD_ATTRIBUTE_LIST&gt;&gt;(\r\n        static_cast&lt;LPPROC_THREAD_ATTRIBUTE_LIST&gt;(::HeapAlloc(::GetProcessHeap(), 0, size)));\r\n    RETURN_IF_NULL_ALLOC(p);\r\n    RETURN_IF_WIN32_BOOL_FALSE(InitializeProcThreadAttributeList(p.get(), attributeCount, 0, &amp;size));\r\n    *result = p.release();\r\n    return S_OK;\r\n}\r\n\r\n\/\/ New overload that takes a list of attributes to preload,\r\n\/\/ with room for any additional attributes you want to add later.\r\n\r\ntemplate&lt;typename C&gt;\r\nHRESULT make_proc_thread_attribute_list_nothrow(\r\n    C&amp;&amp; attributes, DWORD extraAttributeCount,\r\n    _Out_ LPPROC_THREAD_ATTRIBUTE_LIST* result)\r\n{\r\n    *result = nullptr;\r\n    unique_proc_thread_attribute_list list;\r\n    RETURN_IF_FAILED(make_proc_thread_attribute_list_nothrow(\r\n        static_cast&lt;DWORD&gt;(attributes.size()) + extraAttributeCount,\r\n        list.put()));\r\n    RETURN_IF_FAILED(update_proc_thread_attribute_list_nothrow(\r\n        list.get(), std::forward&lt;C&gt;(attributes)));\r\n\r\n    *result = list.release();\r\n    return S_OK;\r\n}\r\n\r\n\/\/ New overload that takes a list of attributes to preload,\r\n\/\/ with no room for more.\r\n\r\ntemplate&lt;typename C&gt;\r\nstd::enable_if_t&lt;!std::is_integral_v&lt;C&gt;, HRESULT&gt;\r\n    make_proc_thread_attribute_list_nothrow(\r\n        C&amp;&amp; attributes,\r\n        _Out_ LPPROC_THREAD_ATTRIBUTE_LIST* result)\r\n{\r\n    return make_proc_thread_attribute_list_nothrow(\r\n        std::forward&lt;C&gt;(attributes), 0, result);\r\n}\r\n<\/pre>\n<p>Note that without the <code>std::enable_if_t<\/code> on the third overload, we would have an ambiguity if somebody called <code>make_<wbr \/>proc_<wbr \/>thread_<wbr \/>attribute_<wbr \/>list_<wbr \/>nothrow(1, p)<\/code> because the parameter <code>1<\/code> would satisfy both the <code>DWORD<\/code> parameter from the first overload as well as matching the third overload with <code>C = int<\/code>. To force the third one to be rejected, we use SFINAE to make the return type a substitution failure if the parameter is integral.<\/p>\n<p>We can then build a throwing version out of the nonthrowing version.<\/p>\n<pre>unique_proc_thread_attribute_list\r\n    make_proc_thread_attribute_list(DWORD attributeCount)\r\n{\r\n    unique_proc_thread_attribute_list result;\r\n    THROW_IF_FAILED(make_proc_thread_attribute_list_nothrow(\r\n        attributeCount, result.put()));\r\n    return result;\r\n}\r\n\r\ntemplate&lt;typename C = std::initializer_list&lt;proc_thread_attribute&gt;&gt;\r\nstd::enable_if_t&lt;!std::is_integral_v&lt;C&gt;, unique_proc_thread_attribute_list&gt;\r\n    make_proc_thread_attribute_list(\r\n        C&amp;&amp; attributes, DWORD extraAttributeCount = 0)\r\n{\r\n    unique_proc_thread_attribute_list result;\r\n    THROW_IF_FAILED(make_proc_thread_attribute_list_nothrow(\r\n        std::forward&lt;C&gt;(attributes), extraAttributeCount,\r\n        result.put()));\r\n    return result;\r\n}\r\n<\/pre>\n<p>We use a defaulted parameter to collapse the &#8220;collection initializer&#8221; and &#8220;collection initializer with additional space&#8221; overloads into one. We still need to use SFINAE to avoid an ambiguity that tries to treat a sole integer parameter as a collection.<\/p>\n<p>You can use this to build process\/thread attribute lists at one go.<\/p>\n<pre>HANDLE handles[2] = { handle1, handle2 };\r\nDWORD protection = PROTECTION_LEVEL_SAME;\r\nauto list = make_proc_thread_attribute_list({\r\n    { PROC_THREAD_ATTRIBUTE_HANDLE_LIST, &amp;handles, sizeof(handles) },\r\n    { PROC_THREAD_ATTRIBUTE_PROTECTION_LEVEL, &amp;protection, sizeof(protection) },\r\n});\r\n<\/pre>\n<p>Or you can build it up with some premade attributes, and others that you add conditionally:<\/p>\n<pre>HANDLE handles[2] = { handle1, handle2 };\r\nDWORD protection = PROTECTION_LEVEL_SAME;\r\nauto list = make_proc_thread_attribute_list({\r\n    { PROC_THREAD_ATTRIBUTE_HANDLE_LIST, &amp;handles, sizeof(handles) },\r\n    { PROC_THREAD_ATTRIBUTE_PROTECTION_LEVEL, &amp;protection, sizeof(protection) },\r\n}, 1); \/\/ \"1\" leaves room for one more attribute\r\nif (job != nullptr) {\r\n    UpdateProcThreadAttribute(list.get(),\r\n        PROC_THREAD_ATTRIBUTE_JOB_LIST,\r\n        &amp;job, sizeof(job), nullptr, nullptr);\r\n}\r\n<\/pre>\n","protected":false},"excerpt":{"rendered":"<p>Another RAII-style wrapper.<\/p>\n","protected":false},"author":1069,"featured_media":111744,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[25],"class_list":["post-112611","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-oldnewthing","tag-code"],"acf":[],"blog_post_summary":"<p>Another RAII-style wrapper.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/112611","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/users\/1069"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/comments?post=112611"}],"version-history":[{"count":1,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/112611\/revisions"}],"predecessor-version":[{"id":112612,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/112611\/revisions\/112612"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media\/111744"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media?parent=112611"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/categories?post=112611"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/tags?post=112611"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}