As I noted at the start of this series, React Native for Windows has to deal with two axes of agility.
- It needs to compile successfully across different versions of the Windows SDK.
- It needs to run successfully across different versions of Windows, while taking advantage of new features if available.
The second is a common scenario, and the typical way of solving it is to probe for the desired feature and use it if is available.
The first is less common. Usually, you control the version of the Windows SDK that your project consumes. The act of ingesting a new version is often considered a big deal.
Libraries like React Native for Windows have to deal with this problem, however, because the project that consumes them gets to pick the Windows SDK version, and the library has to cope with whatever it’s given. In such cases, a feature is used if it is available both in the Windows SDK that the project was compiled with, as well as in the version of Windows that the project is running on.
Not controlling the version of the Windows SDK means that you need to infer at compile time what features are available. The call_
helper fits the bill, but we can go even further to make it even more convenient.if_
defined
For example, consider the Windows.
class. Support for the UI.
Xaml.
UIElement
StartÂBringÂIntoÂView
method was added in interface IUIElement5
, which arrived in the Creators Update.¹ You could write this:
void BringIntoViewIfPossible(UIElement const& e) { auto el5 = e.try_as<IUIElement5>(); if (el5) { el5.StartBringIntoView(); } }
This works great provided the host project is compiling the library with a version of the Windows SDK that contains a definition for IUIElement5
in the first place.
Boom, textbook case for call_
.if_
defined
namespace winrt::Windows::UI::Xaml { struct IUIElement5; } using namespace winrt::Windows::UI::Xaml; void BringIntoViewIfPossible(UIElement const& e) { call_if_defined<IUIElement5>([&](auto* p) { using IUIElement5 = std::decay_t<decltype(*p)>; auto el5 = e.try_as<IUIElement5>(); if (el5) { el5.StartBringIntoView(); } }); }
This type of “probe for interface support” is a common scenario when writing version-agile code, so we could make a specialized version of call_
to simplify the scenario.if_
defined
template<typename T, typename TLambda> void call_if_supported(IInspectable const& source, TLambda&& lambda) { if constexpr (is_complete_type_v<T>) { auto t = source.try_as<T>(); if (t) lambda(std::move(t)); } }
This version calls the lambda if the specified type is (1)Â supported in the SDK being consumed, and (2)Â supported at runtime by the version of Windows that the code is running on. You would use it like this:
void BringIntoViewIfPossible(UIElement const& e) { call_if_supported<IUIElement5>(e, [&](auto&& el5) { el5.StartBringIntoView(); }); }
The idea here is that call_
checks for both compile-time and runtime support, and if both tests pass, it calls the lambda, passing an actual object rather than a dummy parameter.if_
supported
Passing an actual object means that the lambda doesn’t need to re-infer the type. It can just use the passed-in object directly.
This lets you write code that is conditional both on compile-time and runtime feature detection.
A case that you might find useful even if you don’t use C++/WinRT is declaring a variable or member of a particular type, provided it exists.
struct empty {}; template<typename T, typename = void> struct type_if_defined { using type = empty; }; template<typename T> struct type_if_defined<T, std::void_t<decltype(sizeof(T))>> { using type = T; }; template<typename T> using type_if_defined = typename type_or_empty<T>::type;
You can declare a variable or member of type type_
if_
defined
, and it will either contain the thing (if it is defined), or it will be an empty struct. You could combine this with call_
if_
defined
so you have a place to put the thing-that-might-not-be-defined across two calls.
// If "special" is available, preserve it. type_if_defined<special> s; call_if_defined<special>([&](auto *p) { using special = std::decay_t<decltype(*p)>; s = special::get_current(); }); do_something(); call_if_defined<special>([&](auto *p) { using special = std::decay_t<decltype(*p)>; special::set_current(s); });
¹ Who names these things?
Why is requiring the latest Windows SDK such a big deal? Are there breaking changes or dropped features in the newer releases? Open source projects 9ften have quite strict version requirements for their dependencies, why cannot an explicit version of Windows SDK be one of these?
What if somebody wants to use your open source project (which stipulates that it must use the 1703 SDK), but they also want to use some new features added after 1703? Are they forced to forego any Windows features added after 1703?
I think the assumption here is that a later SDK than specified can always be used, only an earlier one can’t.
If that is not the case, things get complicated.
Earlier comment from “Me Gusta” suggests that overshooting can be a problem. (I don’t believe it is, but that’s the claim.) Even if overshooting is okay, you then revisit the problem at the heart of this series: Detecting whether a newer SDK is being used, and taking advantage of its features if so.
Detecting whether a newer SDK is being used, and taking advantage of its features if so.
This won't happen if your library requires the latest SDK that has been available at the time you released your library. You cannot take advantage of features that dd not exist when you were running the final tests.
No, you don't want to force people to use version 1703, but you cannot be prepared to take advantage of the...
If you upgrade your SDK to 1703, then it becomes possible to invoke 1703 features by mistake and break your app’s intended 1702 back compat. Also, sizeof structures may change in 1703, which would prevent the app from running on 1702.
@Raymond, that's exactly what I am trying to understand.
When I upgrade my SDK to 1703, my library can still run on older versions of the platform, can't I? To ensure this, I must sniff whether a feature is supported at runtime.When I use 1702 SDK, I can use my library on newer versions of the platform, but I cannot take advantage of the features that were introduced after 1702, no matter how smart my...
A new SDK can break “binary compatibility” and has done so in the past if you do:
`THING thing = { sizeof(THING), … };` and increase the WINVER related defines without testing. Win2000 did this with OPENFILENAME etc.
I remember these unfortunate glitches of 20 years ago. I believe that Microsoft remembers them even better than yours truly, and has a strong incentive not to allow them anymore.
There can be breaking changes. The UWP API has been renaming or removing things in particular, so it is possible for a build to break.
Can you name some breaking changes that would be points of concern? All of the removals I know about are for APIs that app compat analysis indicates are not being used by anyone.
Kudos, the final culmination of this serie is just phenomenal. I’ve taken a course on C++17 (with some hint of C++2x) this spring and we’ve covered such advance template tricks, but I’m not sure I’d been able to crack the case so cleanly.