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/
/en-us/dotnet/standard/generics/covariance-and-contravariance
/en-us/cpp/cpp/how-to-create-and-use-shared-ptr-instances
/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>