Search code examples
visual-c++concurrencytaskc++-cxppl

Are shared pointers necessary in continuation chains?


I have a continuation chain using lambda expressions where one task assigns to a variable and the next task reads from that variable. Microsoft suggests using a shared_ptr to wrap the variable even when the variable is a reference-counted handle (^). Wouldn't a reference-counted handle increment its reference count when captured by value by the lambda expression? Why then is it necessary to wrap a reference-counted handle with a shared_ptr?


Solution

  • The documentation makes it clear that the cases they are concerned about are ones where

    one task in a continuation chain assigns to a variable, and another task reads that variable

    (Emphasis is mine.) This is not a question of object lifetime, but rather a question of object identity.

    Take this example from the Hilo project, paying close attention to the decoder variable (which is a shared_ptr<BitmapDecoder^>):

    task<InMemoryRandomAccessStream^> ThumbnailGenerator::CreateThumbnailFromPictureFileAsync(
        StorageFile^ sourceFile, 
        unsigned int thumbSize)
    {
        (void)thumbSize; // Unused parameter
        auto decoder = make_shared<BitmapDecoder^>(nullptr);
        auto pixelProvider = make_shared<PixelDataProvider^>(nullptr);
        auto resizedImageStream = ref new InMemoryRandomAccessStream();
        auto createThumbnail = create_task(
            sourceFile->GetThumbnailAsync(
            ThumbnailMode::PicturesView, 
            ThumbnailSize));
    
        return createThumbnail.then([](StorageItemThumbnail^ thumbnail)
        {
            IRandomAccessStream^ imageFileStream = 
                static_cast<IRandomAccessStream^>(thumbnail);
    
            return BitmapDecoder::CreateAsync(imageFileStream);
    
        }).then([decoder](BitmapDecoder^ createdDecoder)
        {
            (*decoder) = createdDecoder;
            return createdDecoder->GetPixelDataAsync( 
                BitmapPixelFormat::Rgba8,
                BitmapAlphaMode::Straight,
                ref new BitmapTransform(),
                ExifOrientationMode::IgnoreExifOrientation,
                ColorManagementMode::ColorManageToSRgb);
    
        }).then([pixelProvider, resizedImageStream](PixelDataProvider^ provider)
        {
            (*pixelProvider) = provider;
            return BitmapEncoder::CreateAsync(
                BitmapEncoder::JpegEncoderId, 
                resizedImageStream);
    
        }).then([pixelProvider, decoder](BitmapEncoder^ createdEncoder)
        {
            createdEncoder->SetPixelData(BitmapPixelFormat::Rgba8,
                BitmapAlphaMode::Straight,
                (*decoder)->PixelWidth,
                (*decoder)->PixelHeight,
                (*decoder)->DpiX,
                (*decoder)->DpiY,
                (*pixelProvider)->DetachPixelData());
            return createdEncoder->FlushAsync();
    
        }).then([resizedImageStream]
        {
            resizedImageStream->Seek(0);
            return resizedImageStream;
        });
    }
    

    The decoder variable is first defined outside of the continuations, since it is needed in multiple continuations. At that point, its value is null. It is obtained and set within the second continuation, and properties of that object (PixelWidth etc) are used within the fourth continuation.

    Were you to instead have decoder be defined as a BitmapDecoder^, set it to nullptr, and then assign it a value within the second continuation, that change would not propagate to subsequent continuations because the change cannot reflected back to the initial handle (the lambda has made a copy of the handle, essentially copying the memory address 0x00000000).

    In order to update the original version (and subsequent references), you would need an additional indirection (e.g. a BitmapDecoder^*). A shared_ptr<BitmapDecoder^> is one such indirection, and a useful one in that you don't need to manage the lifetime of the pointer unlike with a raw pointer, which is why it is recommended in documentation.

    There are other cases where capturing an Object^ would be sufficient, for example if I created a TextBlock^ outside of my continuation and set some properties of it in the first continuation and read some other properties in a subsequent continuation. In this case, all of the handles are referring to the same underlying object and no continuation is attempting to overwrite the identity of the object itself. (However, as initially mentioned, this is not the use case the documentation is referring to.)