DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Because the DevOps movement has redefined engineering responsibilities, SREs now have to become stewards of observability strategy.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Related

  • Floyd's Cycle Algorithm for Fraud Detection in Java Systems
  • Using JSON Web Encryption (JWE)
  • Linked List Popular Problems Vol. 1
  • JVM Memory Architecture and GC Algorithm Basics

Trending

  • Developers Beware: Slopsquatting and Vibe Coding Can Increase Risk of AI-Powered Attacks
  • What Is Plagiarism? How to Avoid It and Cite Sources
  • Operational Principles, Architecture, Benefits, and Limitations of Artificial Intelligence Large Language Models
  • How To Build Resilient Microservices Using Circuit Breakers and Retries: A Developer’s Guide To Surviving
  1. DZone
  2. Data Engineering
  3. AI/ML
  4. Replacing Unique_ptr With C++17's std::variant — a Practical Experiment

Replacing Unique_ptr With C++17's std::variant — a Practical Experiment

In this article, take a look at how to replace unique_ptr with C++17's std::variant.

By 
Bartłomiej Filipek user avatar
Bartłomiej Filipek
·
Sep. 10, 20 · Tutorial
Likes (4)
Comment
Save
Tweet
Share
15.3K Views

Join the DZone community and get the full member experience.

Join For Free

Some time ago I wrote about a new way to implement runtime polymorphism which is based not on virtual functions but on std::visit and std::variant. Please have a look at this new blog post where I experiment with this approach on my home project. The experiment is more practical than artificial examples.

See advantages, disadvantages and practical code issues.

Intro

The new kind of runtime polymorphism is based on the fact that you can call std::visit and then - at runtime - select the best matching overload for the active type in the variant:

Here's a code sample which summarises this technique:

Java
 




x
24


 
1
struct A {
2
    void PrintName() const { 
3
        std::cout << "calling A!\n"
4
    }
5
};
6
 
           
7
struct B {
8
    void PrintName() const { 
9
        std::cout << "calling B!\n"
10
    }
11
};
12
 
           
13
struct CallPrintName {
14
    void operator()(const A& a) { a.PrintName(); }    
15
    void operator()(const B& b) { b.PrintName(); }    
16
};
17
 
           
18
std::variant<A, B> var;
19
var = B{};
20
std::visit(CallPrintName{}, var);
21
 
           
22
// alternative (with a generic lambda):
23
auto callPrintName = [](const auto& obj) { obj.PrintName(); };
24
std::visit(callPrintName, var);


As you can see, we have two classes (unrelated, with just a similar member function name) and we "pack" them into a single std::variant which can represent the first or the second type. Then when we want to call a given member function, we need to create a function object which handles both types (we can also create a generic lambda).

What are the advantages?

  • No dynamic allocation to create a polymorphic class
  • Value semantics, variant can be easily copied
  • Easy to add a new "method", you have to implement a new callable structure. No need to change the implementation of classes
  • There's no need for a base class, classes can be unrelated
  • Duck typing: while virtual functions need to have the same signatures, it's not the case when you call functions from the visitor. They might have a different number of argument, return types, etc. So that gives extra flexibility.

You can read more in: Bartek's coding blog: Runtime Polymorphism with std::variant and std::visit

Let's try to implement this approach on my project, is this as easy as it sounds on an artificial example?

What to Change in the Project

My project (sorting algorithms visualization, C++, WinApi, OpenGL, see at github) has a notion of algorithm manager class which has an "active" algorithm.

This active algorithm is just a unique pointer to IAlgorithm - a base class for all available algorithms:

Java
 




xxxxxxxxxx
1


 
1
CBubbleSortAlgorithm,
2
CShakerSortAlgorithm,
3
CSelectionSortAlgorithm,
4
CInsertionSortAlgorithm,
5
CShellSortAlgorithm,
6
CQuickSortAlgorithm,
7
CShuffleElementsAlgorithm


When a user changes the algorithm from the menu I need to update my pointer to base class so it points to a new algorithm.

Naturally, I selected virtual polymorphism as it's easy to implement and work with. But this place is also a good candidate to experiment with std::variant.

So I can create the following variant:

Java
 




xxxxxxxxxx
1


 
1
using AlgorithmsVariant = std::variant<
2
    CBubbleSortAlgorithm,
3
    CShakerSortAlgorithm,
4
    CSelectionSortAlgorithm,
5
    CInsertionSortAlgorithm,
6
    CShellSortAlgorithm,
7
    CQuickSortAlgorithm,
8
    CShuffleElementsAlgorithm
9
>;


See Bartek's coding blog: Everything You Need to Know About std::variant from C++17 if you want to know more about std::variant.

Ok, so let's make some comparisons:

Size

The first thing that you can observe is that we don't need any v-table pointers so that we can make class smaller (a bit):

Java
 




xxxxxxxxxx
1
14


 
1
// with virtual functions
2
Debug x64
3
sizeof(IAlgorithm): 80
4
sizeof(CBubbleSortAlgorithm): 96
5
sizeof(CInsertionSortAlgorithm): 104
6
sizeof(CSelectionSortAlgorithm): 104
7
sizeof(CQuickSortAlgorithm): 160 
8
 
           
9
Release x64
10
sizeof(IAlgorithm): 72
11
sizeof(CBubbleSortAlgorithm): 88
12
sizeof(CInsertionSortAlgorithm): 96
13
sizeof(CSelectionSortAlgorithm): 96
14
sizeof(CQuickSortAlgorithm): 152 


After changing into variant:

Java
 




xxxxxxxxxx
1
15


 
1
Debug x64
2
sizeof(IAlgorithm): 72
3
sizeof(CBubbleSortAlgorithm): 88
4
sizeof(CInsertionSortAlgorithm): 96
5
sizeof(CSelectionSortAlgorithm): 96
6
sizeof(CQuickSortAlgorithm): 152 
7
sizeof(AlgorithmsVariant): 160
8
 
           
9
Release x64
10
sizeof(IAlgorithm): 64
11
sizeof(CBubbleSortAlgorithm): 80
12
sizeof(CInsertionSortAlgorithm): 88
13
sizeof(CSelectionSortAlgorithm): 88
14
sizeof(CQuickSortAlgorithm): 144
15
sizeof(AlgorithmsVariant): 152


The size between debug and release changes because of the string: sizeof(string): 32 in Release and 40 in Debug.

One note: while the classes are smaller, you need to remember that std::variant composed of those unrelated types will need the max size of them, plus one field for the "discriminator" (currently active index).

We don't have v-pointer so how can we call a function on that variant object? It's not as easy as with a virtual dispatch.

How To Call a Member Function?

With unique_ptr you can just call a virtual function:

Java
 




xxxxxxxxxx
1


 
1
AlgManager::RunAgain() {
2
    currentAlgPtr->Init(m_viArrayCurrent); // reset
3
}


But how to do it with std::variant?

The basic idea is to use std::visit and then pass a generic lambda that calls the proper member function:

Java
 




xxxxxxxxxx
1


 
1
AlgManager::RunAgain() {
2
    auto InitCaller = [](auto& obj ) { obj.Init(??); }
3
    std::visit(InitCaller, currentAlgorithm);
4
}


In the above example, we perform runtime polymorphism by leveraging the visit technique. In short, this function selects the best function overload based on the active type in the variant. Having a generic lambda allows us to have a simple way to call the same function for all possible types in the variant. This is, however, achieved through duck typing.

Problem: Passing Arguments

If you noticed, I put ?? in the generic lambda. This is because there's no easy way to pass a parameter to the function from std::visit!

To solve the issue we can capture the argument into out lambda:

Java
 




xxxxxxxxxx
1


 
1
AlgManager::RunAgain() {
2
    auto InitCaller = [&m_viArrayCurrent](auto& obj ) { obj.Init(m_viArrayCurrent); }
3
    std::visit(InitCaller, currentAlgorithm);
4
}


The code is straightforward for simple built-in types, pointers or references, but it might be problematic when you have some larger objects (we'd like to forward the arguments, not copy them if possible).

Problem: Where to Store Lambdas?

Ok, but there might be several places where you want to call the Init function on the current algorithm, for example in two or more member functions of the Algorithm Manager class. In that case, you'd have to write your lambdas twice, or store them somewhere.

You cannot store it (easily) as a static member of a class as there's no auto type deduction available. You can keep them as static variables in a given compilation unit.

For my experiments I skipped lambdas and went for function objects that are declared in the IAlgorithm class:

Java
 




xxxxxxxxxx
1
30


 
1
class IAlgorithm {
2
public:
3
    struct InitFn {
4
        CViArray<float>* viData;
5
        template<typename T>
6
        inline void operator()(T& alg) const { alg.Init(viData); }
7
    };
8
 
           
9
    struct StepFn {
10
        template<typename T>
11
        inline void operator()(T& alg) const { alg.Step(); }
12
    };
13
 
           
14
    struct GetNameFn {
15
        template<typename T>
16
        inline const std::string& operator()(const T& alg) const { return alg.GetName(); }
17
    };
18
 
           
19
    struct IsDoneFn {
20
        template<typename T>
21
        inline bool operator()(const T& alg) const { return alg.IsDone(); }
22
    };
23
 
           
24
    struct GetStatsFn {
25
        template<typename T>
26
        inline const AlgOpsWrapper& operator()(const T& alg) const { return alg.GetStats(); }
27
    };
28
public:
29
     // ctors and the rest of the interface...
30
};


And now, in all places where you'd like to call a member function of an algorithm you can just write:

Java
 




xxxxxxxxxx
1


 
1
void CAlgManager::RunAgain() {
2
    std::visit(IAlgorithm::InitFn{ &m_viArrayCurrent }, m_CurrentAlg);
3
}
4
 
           
5
void CAlgManager::SetAlgorithm(uint16_t algID) {
6
    m_CurrentAlg = AlgorithmFactory::Create(algID);
7
    std::visit(IAlgorithm::InitFn{ &m_viArrayCurrent }, m_CurrentAlg);
8
}


Is that the best way?

Copyable Again

CAlgManager had a unique_ptr as a data member. To make this class copyable, I had to define copy/move constructors. But with std::variant it's not the case!

With std::variant your classes have value semantics out of the box.

Source Code

All the code is available on my repo; there's a separate branch for this experiment:

https://github.com/fenbf/ViAlg-Update/tree/variant

Summary

Let's compare the outcome, how about the positive side:

  • value type, no dynamic memory allocation (no unique or smart pointers needed)
  • copyable types, no unique_ptr issues
  • no need to v-table, so smaller objects (if that's important)

But how about the negative side:

  • function objects - where to put them?
  • need to add types to using AlgorithmsVariant = std::variant<... explicitly
  • duck typing sometimes can be painful, as the compiler cannot warn you about available methods of a given class (maybe this could be improved with concepts?)
  • no override use, so the compiler cannot report issues with derived classes and their lack of full interface implementation
  • no pure virtual functions - you cannot restrict the real "interface" on your derived classes

So... was this a right approach?

Not sure, as it was quite painful to get everything working.

It would be good to see other use cases where you have, for example, a vector of unique pointers. Replacing this to a vector of variant can reduce lots of small dynamic allocations.

Anyway, I did those experiments so you can see the "real" code and "real" use case rather than nice artificial examples. Hope it helps when you'd like to apply this pattern in your projects.

Let us know your experience in comments below the article.

Java (programming language) Algorithm

Published at DZone with permission of Bartłomiej Filipek, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Floyd's Cycle Algorithm for Fraud Detection in Java Systems
  • Using JSON Web Encryption (JWE)
  • Linked List Popular Problems Vol. 1
  • JVM Memory Architecture and GC Algorithm Basics

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!