Pages

Saturday, July 25, 2009

Virtual base classes (Diamond problem)

Эта статья опубликована на сайте Experts-Exchange в разделе MS Visual C++.

The following diagram presents a diamond class hierarchy:


As depicted, diamond inheritance denotes when two classes (e.g., CDerived1 and CDerived2), separately extending a common base class (e.g., CBase), are sub classed simultaneously by a fourth class (e.g., CTest).

The following is how would expect the diagram to be implemented:

#include <stdio.h>
class CBase
{
public:
virtual void test()
{
printf("CBase::test()\r\n");
}
};

class CDerived1 : public CBase
{
};

class CDerived2 : public CBase
{
};

class CTest : public CDerived1, public CDerived2
{
};

int main()
{
CTest test;
test.test();
return 0;
}

However, the Microsoft VC+ 2005 .NET compiler gives the following message:

1>-- Build started: Project: vft_test, Configuration: Debug Win32 --
1>Compiling...
1>main_romb.cpp
1>c:\source\vft_test\vft_test\main_romb.cpp(27) : error C2385: ambiguous access of 'test'
1> could be the 'test' in base 'CBase'
1> or could be the 'test' in base 'CBase'
1>c:\source\vft_test\vft_test\main_romb.cpp(27) : error C3861: 'test': identifier not found
1>Build log was saved at "file://c:\Source\vft_test\vft_test\Debug\BuildLog.htm"
1>vft_test - 2 error(s), 0 warning(s)
====== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ===

Since CDerived1 and CDerived2 can have very different implementations of CBase::test(), the compiler is unsure which CTest derives. This is the so called "Diamond problem" or even "Dreaded Diamond".


How to deal with this "dread"?
The following code will give you a clue:

#include <stdio.h>
class CBase
{
public:
CBase()
{
printf("CBase::CBase()\r\n");
}
virtual void test()
{
printf("CBase::test()\r\n");
}
};

class CDerived1 : public CBase
{
public:
CDerived1()
{
printf("CDerived1::CDerived1()\r\n");
}
};

class CDerived2 : public CBase
{
public:
CDerived2()
{
printf("CDerived2::CDerived1()\r\n");
}
};

class CTest : public CDerived1, public CDerived2
{
public:
CTest()
{
printf("CTest::CTest()\r\n");
}
};

int main()
{
CTest test;
//test.test();
return 0;
}



This program is compiled successfully and works:


Of course you know how CTest object is created. It derives firstly from CDerived1 object that inherits from CBase, and, secondly, CTest derives from CDerived2 that also inherits from CBase. So the complier (probably any compiler) creates CBase object, then CDerived1, and now it needs to create CBase again, …, etc., we got 2 CBase objects! The diagram didn’t show that CBase class appears twice in the class hierarchy.
C++ provides a technique for such case. The declaration of the CBase class as virtual solves the situation:

#include <stdio.h>

class CBase
{
public:
CBase()
{
printf("CBase::CBase()\r\n");
}
virtual void test()
{
printf("CBase::test()\r\n");
}
};

class CDerived1 : virtual public CBase
{
public:
CDerived1()
{
printf("CDerived1::CDerived1()\r\n");
}
};

class CDerived2 : virtual public CBase
{
public:
CDerived2()
{
printf("CDerived2::CDerived1()\r\n");
}
};

class CTest : public CDerived1, public CDerived2
{
public:
CTest()
{
printf("CTest::CTest()\r\n");
}
};

int main()
{
CTest test;
test.test();
return 0;
}



The application output is shown on the following screenshot:

In this example CBase is inherited virtually. CDerived1 and CDerived2 classes share the same implementation of CBase. The compiler creates only one CBase object.
Of course, it is possible to solve this “Diamond problem” in another ways. For example like that:

#include <stdio.h>

class CBase
{
public:
virtual void test()
{
printf("CBase::test()\r\n");
}
};

class CDerived1 : public CBase
{
};

class CDerived2 : public CBase
{
};

class CTest : public CDerived1, public CDerived2
{
};

int main()
{
CTest test;
test.CDerived1::test();
return 0;
}


I think that the best solution is to try to avoid such diamond problems. That will keep the design simple.
The following source code shows how we can use aggregation instead of the diamond inheritance with our same classes:

#include <stdio.h>

class CBase
{
public:
virtual void test()
{
printf("CBase::test()\r\n");
}
};

class CDerived1 : public CBase
{
};

class CDerived2 : public CBase
{
};

class CTest
{
CDerived1 m_One;
CDerived2 m_Two;
public:
void test()
{
m_One.test();
m_Two.test();
//or just
//m_One.test();
}
};

int main()
{
CTest test;
test.test();
return 0;
}


You see that this way makes CTest objects bigger in the memory - it contains of CDerived1 and CDerived2 and which of them has own CBase.

No comments:

Post a Comment