October 8th, 2020

An iterable of iterables: C# collections support covariance, but C++ collections do not

Collections in the C# language support covariance. Collections in the C++ language do not.

This means, for example, that if a function wants a collection of T, C# lets you pass a collection of things that derive from T, but C++ requires you to pass a collection of exactly T.

This can be confusing for methods that accept collections of collections. C++ will allow the outer collection to decay, but the objects inside the collection cannot. The Windows Runtime by convention accepts collections of T in the form of an IIterable<T> (which projects into C# as of an IEnumerable<T>), so that you can pass anything that can produce a sequence of T objects. It could be a std::vector<T> or a std::array<T> or even a SQL or LINQ query that produces a sequence of T objects.

But things get a little weird if you have a collection of collections, because C++ collections don’t support covariance.

Before we look at covariance, let’s look at simple conversion. Suppose you have to pass an IIterable<Point> to a method like Ink­Manager.Select­With­Poly­Line which takes a parameter of type IIterable<Point>.

// C#
var points = new List<Point> {
        UpperLeftCorner, UpperRightCorner, LowerRightCorner, LowerLeftCorner
    };
inkManager.SelectWithPolyline(points);

// C++/CX
auto points = ref new Vector<Point>({
      UpperLeftCorner, UpperRightCorner, LowerRightCorner, LowerLeftCorner,
});
inkManager->SelectWithPolyline(points);

// C++/WinRT
auto points = single_threaded_vector<Point>({
      UpperLeftCorner, UpperRightCorner, LowerRightCorner, LowerLeftCorner,
  });
inkManager.SelectWithPolyline(points);

This works because List<T> is convertible to IEnumerable<T>, and Vector<T> is convertible to IIterable<T>. The conversion works one level deep.

Now suppose you are implementing the UIElement.Find­Sub­Elements­For­Touch­Targeting:

// C#
virtual override IEnumerable<IEnumerable<Point>> FindSubElementsForTouchTargeting();

// C++/CX
virtual IIterable<IIterable<Point>^>^ FindSubElementsForTouchTargeting() override;

// C++/WinRT
IIterable<IIterable<Point>> FindSubElementsForTouchTargeting();

In C#, this is straightforward. You can have a list of lists:

virtual override IEnumerable<IEnumerable<Point>> FindSubElementsForTouchTargeting()
{
  var polygon1 = new List<Point> {
        UpperLeftCorner1, UpperRightCorner1, LowerRightCorner1, LowerLeftCorner1,
    };
  var polygon2 = new List<Point> {
        UpperLeftCorner2, UpperRightCorner2, LowerRightCorner2, LowerLeftCorner2,
    };
  var results = new List<List<Point>> { polygon1, polygon2 };
  return results;
}

Thanks to covariance, a List<List<Point>> is compatible with IEnumerable<IEnumerable<Point>> because List<T> is compatible with IEnumeraable<T>, and IEnumeraable<T> is covariant in T.

C++ is not as lucky. The analogous code in C++/CX would be something like this:

virtual override IEnumerable<IEnumerable<Point>> FindSubElementsForTouchTargeting()
{
  auto polygon1 = ref new Vector<Point>({
        UpperLeftCorner1, UpperRightCorner1, LowerRightCorner1, LowerLeftCorner1,
    });
  auto polygon2 = ref new Vector<Point>({
        UpperLeftCorner2, UpperRightCorner2, LowerRightCorner2, LowerLeftCorner2,
    });
  auto results = ref new Vector<Vector<Point>>({ polygon1, polygon2 });
  return results;
}

And in C++/WinRT:

IIterable<IIterable<Point>> FindSubElementsForTouchTargeting()
{
  auto polygon1 = single_threaded_vector<Point>({
        UpperLeftCorner1, UpperRightCorner1, LowerRightCorner1, LowerLeftCorner1,
    });
  auto polygon2 = single_threaded_vector<Point>({
        UpperLeftCorner2, UpperRightCorner2, LowerRightCorner2, LowerLeftCorner2,
    });
  auto results = single_threaded_vector<IVector<Point>>({ polygon1, polygon2 });
  return results;
}

This code doesn’t work because there is no conversion from Vector<Vector<Point>^>^ to IIterable<IIterable<Point>^>^ (C++/CX) or from IVector<IVector<Point>> to IIterable<IIterable<Point>> (C++/WinRT). The automatic conversion gets you part way there, but it can’t convert the inner portion due to the lack of covariance.

C++/CX C++/WinRT
Vector< Vector< Point>^>^ IVector< IVector <Point>>
       
IIterable< Vector< Point>^>^ IIterable< IVector <Point>>
 
   
 
IIterable< IIterable< Point>^>^ IIterable< IIterable< <Point>>

You have to declare the inner portion exactly correctly the first time. The language isn’t going to help you.

// C++/CX
virtual override IEnumerable<IEnumerable<Point>> FindSubElementsForTouchTargeting()
{
  auto polygon1 = ref new Vector<Point>({
        UpperLeftCorner1, UpperRightCorner1, LowerRightCorner1, LowerLeftCorner1,
    });
  auto polygon2 = ref new Vector<Point>({
        UpperLeftCorner2, UpperRightCorner2, LowerRightCorner2, LowerLeftCorner2,
    });
  auto results = ref new Vector<IEnumerable<Point>>({ polygon1, polygon2 });
  return results;
}

And in C++/WinRT:

IIterable<IIterable<Point>> FindSubElementsForTouchTargeting()
{
  auto polygon1 = single_threaded_vector<Point>({
        UpperLeftCorner1, UpperRightCorner1, LowerRightCorner1, LowerLeftCorner1,
    });
  auto polygon2 = single_threaded_vector<Point>({
        UpperLeftCorner2, UpperRightCorner2, LowerRightCorner2, LowerLeftCorner2,
    });
  auto results = single_threaded_vector<IIterable<Point>>({ polygon1, polygon2 });
  return results;
}

You do lose the ability to index the results and access the original vectors, since by the time you put them into the results, they have been turned into IIterables.

C++/WinRT gives you a little help here. If the iterable of iterables is a parameter to a function, then you can let C++/WinRT auto-generate the outer iterable, at which point the automatic conversions will kick in for the inner objects because the type is being explicitly specified by the parameter.

extern void SomeMethod(winrt::param::iterable<
    IIterable<Point>> const& elementOutlines);

auto polygon1 = single_threaded_vector<Point>({
      UpperLeftCorner1, UpperRightCorner1, LowerRightCorner1, LowerLeftCorner1,
  });
auto polygon2 = single_threaded_vector<Point>({
      UpperLeftCorner2, UpperRightCorner2, LowerRightCorner2, LowerLeftCorner2,
  });
SomeMethod({ polygon1, polygon2 });

That wasn’t too messy.

Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

2 comments

Discussion is closed. Login to edit/delete existing comments.

  • Neil Rashbrook

    Your table uses for C++/CX, but your text uses (or sometimes ).

    It might be possible to create a user-defined conversion operator from into a , but it still doesn't directly help you because that's two casts away from your original so it won't happen automatically.

    Read more
  • Alexis Ryan

    the covariance in c# seems a bit weird compared to other languages but it’s a nice feature to have and its something a beginner to generics might assume is how things should work