ned14 / status-code

Proposed SG14 status_code for the C++ standard

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Usability issue with implicit/explicit status_code construction and assignment

jwtowner opened this issue · comments

Hi Niall!

So, we've run into an issue with the status_code template class where it seems a little inconsistent with regards to how it only allows explicit construction from value types without a make_status_code overload, while still allowing for implicit assignment from values of the same type.

Below is a code sample modeled after real world code that illustrates the problem. The sample fails to compile when attempting to construct the cc object with the return value from calling CreateDXGIFactory2. Note that further down in the sample function, it's perfectly fine to assign values of the same type as it uses the operator= overload taking a const value_type& argument.

Microsoft::WRL::ComPtr<ID3D12Device> InitializeDevice()
{
  Microsoft::WRL::ComPtr<ID3D12Device> device;

  // Obtain the DXGI factory
  Microsoft::WRL::ComPtr<IDXGIFactory4> dxgiFactory;
  system_error2::com_code cc = CreateDXGIFactory2(0, IID_PPV_ARGS(&dxgiFactory)); // <-- compilation error here, no implicit construction allowed with HRESULT value types
  assert(cc.success());

  // Create the D3D graphics device with the most dedicated video memory
  Microsoft::WRL::ComPtr<IDXGIAdapter1> adapter;
  SIZE_T maxSize = 0;

  for(UINT adapterIndex = 0; DXGI_ERROR_NOT_FOUND != dxgiFactory->EnumAdapters1(adapterIndex, &adapter); ++adapterIndex)
  {
    DXGI_ADAPTER_DESC1 desc;
    cc = adapter->GetDesc1(&desc);
    if(cc.failure() || (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE) != 0 || desc.DedicatedVideoMemory <= maxSize)
      continue;
    cc = D3D12CreateDevice(adapter.Get(), D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&device));
    if(cc.failure())
      continue;
    cc = adapter->GetDesc1(&desc);
    if(cc.failure())
      continue;
    maxSize = desc.DedicatedVideoMemory;
  }

  return device;
}

It seems reasonable to expect that if assignment from a value_type object compiles fine that initialization by assignment should also work without issue. The situation becomes further compounded when you also consider that if you create a custom status_code_domain for an enum with an overloaded make_status_code, implicit construction is enabled and initialization by assignment is allowed. So you end up with implicit construction of status_codes working in some circumstances but not others.

It seems what we really want is to have the ability to enable implicit construction from a value_type object on those status_codes where you can't overload make_status_code because the value_type is an integral or fundamental type that is probably going to be used as the value_type of other status_code_domains.

I took a stab at trying to solve this problem by allowing status_code_domain to provide an optional is_explicitly_constructible type alias of std::bool_constant and using type traits to detect this and enable explicit or implicit constructors and an assignment operator taking a forwarding reference type T, so long as std::is_convertible_v<T&&, value_type> is also true. When is_explicitly_constructible is not provided, it defaults to std::true_type. It should probably also check to make sure T isn't the status_code instantiation itself, but not currently doing that. So basically, I've got something that looks like this in status_code:

template <typename T, typename D = domain_type,
        std::enable_if_t<
            std::is_convertible_v<T&&, value_type> &&
            detail::is_status_code_explicitly_constructible<D>::value, int> = 0>
constexpr explicit status_code(T&& v)
        noexcept( std::is_nothrow_constructible_v<value_type, T&&> )
    : _base(typename _base::_value_type_constructor{}, &domain_type::get(), static_cast<T&&>(v)) {}

template <typename T, typename D = domain_type,
        std::enable_if_t<
            std::is_convertible_v<T&&, value_type> &&
            !detail::is_status_code_explicitly_constructible<D>::value, int> = 0>
constexpr status_code(T&& v)
        noexcept(std::is_nothrow_constructible_v<value_type, T&&>)
    : _base(typename _base::_value_type_constructor{}, &domain_type::get(), static_cast<T&&>(v)) {}

template <typename T, typename D = domain_type,
        std::enable_if_t<
            std::is_convertible_v<T&&, value_type> &&
            !detail::is_status_code_explicitly_constructible<D>::value, int> = 0>
constexpr status_code& operator= T&& v)
        noexcept(std::is_nothrow_assignable_v<value_type, T&&>)
{
    this->_value = static_cast<T&&>(v);
    return *this;
}

In C++20, we of course have explicit(bool) and requires at our disposal to aid in implementing the above, but that's the basic idea.

Looking at the current state of the library, I see now that there is a traits::is_move_bitcopying template class, so I'm wondering if it might be better to externalize things and have a traits::is_explicitly_constructible<StatusCodeDomain>, or a traits::is_implicitly_constructible_from_value_type<StatusCodeDomain> or something else along those lines?

Another benefit with the above is that it also disables the assignment operator from a value_type unless the is_explicitly_constructible trait is set to std::false_type. So now when assigning a value_type object for which there is a make_status_code overload, the compiler will instead use the implicit constructor that utilizes make_status_code.

What are your thoughts on all of this? Do you think we could get something like this integrated into the status-code library and proposed design?

Firstly, thanks for raising this. The assignment operator for value_type should never have been there in the first place, so I just removed it. The defaulted assignment operator would force assignment to run through the constructor overload set, which is what was originally intended.

Regarding the bigger picture you raise though, I think that, generally speaking, you never ever want implicit construction from integrals for status code, these ought to always be explicit. COM codes irritatingly can have the type of void*, so that to me would rule out ever allowing implicit construction from them i.e. your construction by =, I would insist on brackets. For your code segment above, I don't think it's much to ask for com_code(expr) to wrap every COM code assigned, and lots of benefits in terms of less surprise comes with that. Implicit construction is a major source of unpleasant surprise.

I'll stop now, as it is past 11pm, and my brain has clearly turned into putty and I cannot say much which is useful. If removing the implicit assignment was not at all what you wanted, do let me know.

Now that I think about it some more, you're definitely right about implicit construction being a bad thing here, and removing it is probably the way to go. Thanks for pushing back on that feature request. I have removed the assignment operator and the explicit(bool) constructor I was experimenting with from our implementation as well, and set out to fix up our code base.

As I was fixing things up, I came to realize it would be really nice if we had a convenience function to create win32_codes with the result of GetLastError(). It could be a non-member utility function defined outside of the win32_code class. It could be a static member function in win32_code that would call GetLastError and construct and return a win32_code object. Alternatively, it could be another implicit constructor that takes a tag type, and does the same thing. I think it's a very minor improvement, but an improvement nonetheless, just so long as the name of the function or tag type is less than or equal to in character length with "win32_code{GetLastError()}." The name could be something like win32_code::last() for the static member factory function or struct win32_last_code_t{} for the tag type, etc.

Here are some examples.

Old code with status_code assignment operator:

win32_code code;
if (BOOL success = FlushViewOfFile(data, size); UNLIKELY(!success))
	code = GetLastError();
if (BOOL success = UnmapViewOfFile(data); UNLIKELY(!success && !code.failure()))
	code = GetLastError();
// more API calls closing mapping and file here
return code;

With the assignment operator removed:

win32_code code;
if (BOOL success = FlushViewOfFile(data, size); UNLIKELY(!success))
	code = win32_code{GetLastError()};
if (BOOL success = UnmapViewOfFile(data); UNLIKELY(!success && !code.failure()))
	code = win32_code{GetLastError()};
// more API calls closing mapping and file here
return code;

With a non-member factory function that calls GetLastError() and returns a win32_code:

win32_code code;
if (BOOL success = FlushViewOfFile(data, size); UNLIKELY(!success))
	code = get_last_win32_code();
if (BOOL success = UnmapViewOfFile(data); UNLIKELY(!success && !code.failure()))
	code = get_last_win32_code();
// more API calls closing mapping and file here
return code;

With a static member factory function that calls GetLastError() and returns a win32_code:

win32_code code;
if (BOOL success = FlushViewOfFile(data, size); UNLIKELY(!success))
	code = win32_code::last();
if (BOOL success = UnmapViewOfFile(data); UNLIKELY(!success && !code.failure()))
	code = win32_code::last();
// more API calls closing mapping and file here
return code;

With an implicit constructor taking a tag type named system_error2::win32_last_code_t and tag value with same name minus the _t suffix.

win32_code code;
if (BOOL success = FlushViewOfFile(data, size); UNLIKELY(!success))
	code = win32_last_code;
if (BOOL success = UnmapViewOfFile(data); UNLIKELY(!success && !code.failure()))
	code = win32_last_code;
// more API calls closing mapping and file here
return code;

Thoughts? It's okay if you think its too minor of a feature to bother. I imagine users of the library can always define their own non-member free function if they feel the need to do so.

Actually I had intended there to be a mixin for the win32_code which means default construction pulls the value from GetLastError(). Again you have surprised me that code I had thought was the case is not. Sigh.

Thanks once again for pointing out a mistake. I'll fix that shortly.

Ok, there is now a static member function win32_code::current() and posix_code::current(). Does this solve your request?

Yep, those do the trick! Thanks!