Detecting in C++ whether a type is defined, part 5: Augmenting the basic pattern

Raymond Chen

Raymond

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_if_defined helper fits the bill, but we can go even further to make it even more convenient.

For example, consider the Windows.UI.Xaml.UIElement class. Support for the 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_if_defined to simplify the scenario.

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_if_supported 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.

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?

Raymond Chen
Raymond Chen

Follow Raymond   

12 comments

Comments are closed.

  • Avatar
    Pierre Baillargeon

    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.

  • Avatar
    Alex Cohn

    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?

    • Avatar
      Me Gusta

      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.

      • Raymond Chen
        Raymond Chen

        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.

    • Raymond Chen
      Raymond Chen

      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?

      • Avatar
        Sebastian Redl

        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.

        • Raymond Chen
          Raymond Chen

          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.

          • Avatar
            Alex Cohn

            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 new features of 1704 if they choose to work with it.

            call_if_defined

            could help your library survive compilation with SDK 1702, but my question was, what may be the reason for the users not to upgrade their build environment to 1703?

          • Raymond Chen
            Raymond Chen

            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.

          • Avatar
            Alex Cohn

            @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 templates are.

            I am often facing the situation when I must support different versions of SDK (I am speaking about Android NDK here).

            NDK (the C++ toolchain), unfortunately, is different. From time to time, breaking changes have been introduced, e.g. removing gcc compiler, or removing gnustl, or  At the very least, these changes required that the build scripts must be edited. For some developers, who use C++ components ‘as is’ with their Java code, such changes happen to be too painful. That’s why we have to sniff the version of NDK, and make some adaptations to allow the library be used with older releases.

            With Android SDK (Java/Kotlin), it is never has been an issue to use the latest version. I thought that for Windows SDK, backward compatibility is even better.

          • W S
            W S

            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.

          • Avatar
            Alex Cohn

            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.