Store different derived classes in collections in C++ and C#: CoVariance, shared_ptr, unique_ptr


I wanted to create a collection container that would hold objects of various types, all derived from a common interface/base class.
In C# and VB, this is easy. Just create a new List<MyBase>() and add elements to it, including various MyBase derived types
Adding elements that are more derived is Covariance (not to be confused with covariance in statistics).

I wanted to do the same in C++. I’ve been using the STL containers, and knew that I could easily to this by storing references to the objects in e.g. a vector.
This would require my code to manage the creation and destruction of the objects, meaning memory management.
Wouldn’t it be nice if I could write the code without any memory management at all? In other words, no operator new or delete and no “*” operator: just use smart pointers.

std::shared_ptr and std::unique_ptr are really handy here.


Below is a sample showing both C# and C++ covariance in containers
The memory allocation calls (malloc and free) are used only for diagnostics to ensure no memory leaks and can be removed.


Start Visual Studio View->Team Explorer
Local Git Repositories->New “CoVariance”
Solutions->New->C#->Windows Classic Desktop-> Wpf App-> “WpfCoVariance”

Paste in the C# code below to replace MainWIndow.Xaml.cs

Right click Solution in Solution Explorer->Add New Project->C++->Windows Desktop->Windows Desktop Wizard->”CppCovariance”
Application Type-> Windows Application (.exe)
Click on Empty Project Checkbox
CppCovariance->Add->New Item->C++ File
If you get this error:
1>Project not selected to build for this solution configuration
Build->Configuration->CppCovariance->Check the “Build checkbox”
Project->Addd->New Item->CPP Source file->CoVariance.cpp.
Paste in the CppCode below

Right click on CppCovariance in the Solution explorer and choose “Set as Startup Project”


See also
https://blogs.msdn.microsoft.com/calvin_hsia/2010/03/16/use-a-custom-allocator-for-your-stl-container/
https://docs.microsoft.com/en-us/dotnet/standard/generics/covariance-and-contravariance

https://docs.microsoft.com/en-us/cpp/cpp/how-to-create-and-use-shared-ptr-instances
https://docs.microsoft.com/en-us/cpp/cpp/how-to-create-and-use-unique-ptr-instances

<C# Code>

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfCoVariance
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            int numIter = 10;

            var lst = new List<MyBase>();
            for (int i = 0; i < numIter; i++)
            {
                MyBase x = null;
                if (i % 2 == 0)
                {
                    x = new MyDerivedA();
                }
                else
                {
                    x = new MyDerivedB();
                }
                lst.Add(x);
            }
            foreach(var instance in lst)
            {
                var result = instance.DoSomething();
                if (instance is MyDerivedA)
                {
                    Debug.Assert(result == $"{nameof(MyDerivedA)}:{nameof(instance.DoSomething)}", "Didn't get right value");
                }
                else
                {
                    Debug.Assert(result == $"{nameof(MyDerivedB)}:{nameof(instance.DoSomething)}", "Didn't get right value");
                }
            }
        }
    }

    abstract class MyBase
    {
        public abstract string DoSomething();
    }
    class MyDerivedA : MyBase
    {
        public override string DoSomething()
        {
            return $"{nameof(MyDerivedA)}:{nameof(DoSomething)}";
        }
    }
    class MyDerivedB : MyBase
    {
        public override string DoSomething()
        {
            return $"{nameof(MyDerivedB)}:{nameof(DoSomething)}";
        }
    }
}

</C# Code>


<CppCode>

#include "windows.h"
#include "memory"
#include "vector"
#include "string"

using namespace std;

interface MyBase // can use "class" or "struct" here too
{
	//public:  // public needed if it's a "class"
		// these counts, and the _ptrDummyMemBlock are only needed for behavior validation
	static int g_nInstances;
	static LONGLONG g_nTotalAllocated;

	virtual string DoSomething() = 0; // pure virtual
	void *_ptrDummyMemBlock;
	int _size;
	MyBase()
	{
		g_nInstances++;
	}
	virtual ~MyBase() // what happens if you remove the "virtual" ?
	{
		g_nInstances--;
	};
	void DoAllocation(int reqSize)
	{
		_ASSERT_EXPR(reqSize > 4, L"too small");
		// put the Free's in the derived classes to demo virtual dtor
		_ptrDummyMemBlock = malloc(reqSize);
		_size = reqSize;
		g_nTotalAllocated += reqSize;
		_ASSERT_EXPR(_ptrDummyMemBlock != nullptr, L"Not enough memory");
		// store the size of the allocation in the allocated block itself
		*(int *)_ptrDummyMemBlock = reqSize;
	}
	void CheckSize()
	{
		_ASSERT_EXPR(_ptrDummyMemBlock != nullptr, "no memory?");
		// get the size of the allocated block from the block
		auto size = *((int *)_ptrDummyMemBlock);
		_ASSERT_EXPR(_size == size, L"sizes don't match");
	}
};

int MyBase::g_nInstances = 0;
LONGLONG MyBase::g_nTotalAllocated = 0;

class MyDerivedA :
	public MyBase
{
public:
	MyDerivedA(int reqSize)
	{
		DoAllocation(reqSize);
	}
	~MyDerivedA()
	{
		_ASSERT_EXPR(_ptrDummyMemBlock != nullptr, L"_p should be non-null");
		CheckSize();
		free(_ptrDummyMemBlock);
		g_nTotalAllocated -= _size;
		_ptrDummyMemBlock = nullptr;
	}
	string DoSomething()
	{
		CheckSize();
		return "MyDerivedA::DoSomething";
	}
};

class MyDerivedB :
	public	MyBase
{
public:
	MyDerivedB(int reqSize)
	{
		DoAllocation(reqSize);
	}
	~MyDerivedB()
	{
		_ASSERT_EXPR(_ptrDummyMemBlock != nullptr, L"_ptrDummyMemBlock should be non-null");
		free(_ptrDummyMemBlock);
		g_nTotalAllocated -= _size;
		_ptrDummyMemBlock = nullptr;
	}
	string DoSomething()
	{
		CheckSize();
		return "MyDerivedB::DoSomething";
	}
};

void DoTestShared_Ptr(int numIter)
{
	// use shared_ptr to be the owner of the object
	vector<shared_ptr<MyBase >> vecSharedPtr;
	for (int i = 0; i < numIter; i++)
	{
		int numA = 0;
		int numB = 0;
		vecSharedPtr.push_back(make_shared<MyDerivedA>(111));
		vecSharedPtr.push_back(make_shared<MyDerivedB>(222));
		_ASSERT_EXPR(MyBase::g_nInstances == 2, L"should have 2 instances");
		for (auto x : vecSharedPtr)
		{
			auto result = x->DoSomething();
			auto xx = dynamic_pointer_cast<MyDerivedA>(x);
			if (xx != nullptr)
			{
				_ASSERT_EXPR(result == "MyDerivedA::DoSomething", L"should be MyDerivedA");
				numA++;
			}
			else
			{
				_ASSERT_EXPR(dynamic_pointer_cast<MyDerivedB>(x) != nullptr, L"should be MyDerivedB");
				_ASSERT_EXPR(result == "MyDerivedB::DoSomething", L"should be MyDerivedB");
				numB++;
			}
		}
		_ASSERT_EXPR(numA == 1 && numB == 1, L"should have 1 of each instance");
		vecSharedPtr.clear();
		_ASSERT_EXPR(MyBase::g_nInstances == 0, L"should have no instances");
		_ASSERT_EXPR(MyBase::g_nTotalAllocated == 0, L"should have none allocated");
	}
}

void DoTestUnique_Ptr(int numIter)
{
	// use unique_ptr to be the sole owner of the object.
	vector<unique_ptr<MyBase >> vecUniquePtr;
	for (int i = 0; i < numIter; i++)
	{
		vecUniquePtr.push_back(make_unique<MyDerivedA>(123));
		vecUniquePtr.push_back(make_unique<MyDerivedB>(456));
		_ASSERT_EXPR(MyBase::g_nInstances == 2, L"should have 2 instances");
		// because we're using unique_ptr, we must iterate using a ref to the object, 
		// rather than a copy
		for (auto &x : vecUniquePtr)
		{
			auto result = x->DoSomething();
			_ASSERT_EXPR(result == "MyDerivedA::DoSomething" ||
				result == "MyDerivedB::DoSomething"
				, L"should have no instances");
			// no dynamice_pointer_cast for unique_ptr
		}
		vecUniquePtr.clear();
		_ASSERT_EXPR(MyBase::g_nInstances == 0, L"should have no instances");
		_ASSERT_EXPR(MyBase::g_nTotalAllocated == 0, L"should have none allocated");
	}
}

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
	_In_opt_ HINSTANCE hPrevInstance,
	_In_ LPWSTR    lpCmdLine,
	_In_ int       nCmdShow)
{
	UNREFERENCED_PARAMETER(hPrevInstance);
	UNREFERENCED_PARAMETER(lpCmdLine);
	int numIter = 10;
	DoTestShared_Ptr(numIter);
	DoTestUnique_Ptr(numIter);

	MessageBoxA(0, "Done", "CppCovariance", 0);
}

</CppCode>

Comments (0)

Skip to main content