Boost shared_ptr thread safety queries

Ah hello

I am looking at Boost's shared_ptr class, in particular its thread safety.  There are several unknowns to me here but they are all related hence I have grouped them as one question.

Whilst looking at the code that I query in question 2 below, I tried to answer it for myself, but in the process of doing so found that I don't actually know how to pass a shared_ptr to a thread.  This was my initial attempt:

void ThreadFunc(void* pP)
{
	boost::shared_ptr<int>* pParam = (boost::shared_ptr<int>*)pP;
	boost::shared_ptr<int> pS(*pParam);
}

int main(int argc, char* argv[])
{
	boost::shared_ptr<int> pS(new int(100));
	_beginthread( ThreadFunc, 0, (void*)&pS);
	// ...
}

Open in new window

However I realised that if pS goes out of scope in main() before the thread starts, the thread will create a copy of a dead object.  Boom.

Question 1:

How can we pass shared_ptr objects between threads?  Do we need some sync object to ensure that main() waits for the thread to create its copy of the shared_ptr?

Question 2:

I am flumoxed by the following code:

// From boost_1_49_0\boost\smart_ptr\detail\sp_counted_base_w32.hpp
void release() // nothrow
{
	if( BOOST_INTERLOCKED_DECREMENT( &use_count_ ) == 0 )
	{
		dispose();
		weak_release();
	}
}

Open in new window


Suppose thread A and thread B both release at the same time.  Thread A runs to BOOST_INTERLOCKED_DECREMENT(), but doesn't actually start executing it.  It is then pre-empted by thread B which executes the complete method - it decrements use_count_, finds it is zero, then destroys the object.

Thread A then resumes.  This is where I think there may be a problem.  

a) It will access use_count_, which is part of the object which has been destroyed - surely this is an access violation?  

b) Furthermore, what if another object has been allocated - it could be at the same memory location as that which we have just destroyed, hence we are now decrementing the use_count_ member of another object altogether!

c) If, after destruction, the current object just happens to have (randomly initialised) use_count_ as 1, thread A would decrement it and free the same memory again: boom!

TIA
LVL 19
mrwad99Asked:
Who is Participating?

[Product update] Infrastructure Analysis Tool is now available with Business Accounts.Learn More

x
I wear a lot of hats...

"The solutions and answers provided on Experts Exchange have been extremely helpful to me over the last few years. I wear a lot of hats - Developer, Database Administrator, Help Desk, etc., so I know a lot of things but not a lot about one thing. Experts Exchange gives me answers from people who do know a lot about one thing, in a easy to use platform." -Todd S.

sarabandeCommented:
How can we pass shared_ptr objects between threads?
either use a global (or static class member) or pass a pointer to struct or class to the thread where the shared_ptr is a member. then, by casting to void* and back it is not the shared_ptr object that was casted and looses information but the object passed. in both cases shared_ptr was not copied but is still the same object.

Thread A then resumes.  This is where I think there may be a problem.

no. in my opinion the code is safe (beside that i didn't exactly know what dispose and weak_release are doing ...)

the point is that BOOST_INTERLOCKED_DECREMENT is thread-safe (atomar) and actually will do the decrementiation in any case. that means both thread A and B will decrement and the one which gets 0 as return will do the cleaning.

Sara
0
mrwad99Author Commented:
Thanks Sara.

OK, I can see how passing a structure could guarantee the lifetime of the shared_ptr.

However, we want the shared_ptr to be copied; we want the thread to have its own copy, as this is guaranteed to be thread safe:

From http://www.boost.org/doc/libs/1_49_0/libs/smart_ptr/shared_ptr.htm#ThreadSafety

"Different shared_ptr instances can be "written to" (accessed using mutable operations such as operator= or reset) simultaneosly by multiple threads (even when these instances are copies, and share the same reference count underneath.)

Any other simultaneous accesses result in undefined behavior."

The example on this page that is not thread safe is

// thread A
p3.reset(new int(1));

// thread B
p3.reset(new int(2)); // undefined, multiple writes

The example that is thread safe is

// thread A
p.reset(new int(1912)); // writes p

// thread B
p2.reset(); // OK, writes p2

Open in new window


Now, it is not entirely clear, but I am presuming that p2 is a copy of p, hence the bold part of the quote above applies to it.

For this reason, we surely need to take a copy of the shared_ptr within the thread, otherwise we would end up with a situation like the first (not thread-safe) example quoted above!

Regarding the destruction: I understand that BOOST_INTERLOCKED_DECREMENT is atomic, and I actually think I have answered my own question now, and proven why we need to have a copy of the shared_ptr in the thread.  If we pass the same object, it is entirely possible that one thread will destroy the object, then the second thread, which was also in the release() method but got preempted before executing BOOST_INTERLOCKED_DECREMENT() would then access the destroyed member use_count_.  If each thread has its own copy, this cannot happen as no two threads will ever enter release() holding the same value for use_count_.

So, question 1 still remains, unless we do use a global or a static: I think then that we must have to have some sort of sync object that waits for the thread to start so the thread can take its own copy and hence prevent the shared_ptr going out of scope.

Another question has been raised from the quote above:

Different shared_ptr instances can be "written to" (accessed using mutable operations such as operator= or reset) simultaneously by multiple threads (even when these instances are copies, and share the same reference count underneath.)

I have stepped through a call to reset() and there is no locking of any kind that I can see, so whereas I want to believe this statement I cannot see how it can be true.  Can someone please clarify this for me?
0
sarabandeCommented:
i don't think that your sample code hits the point. why should 'p3.reset(new int(2));' be a violation of thread-safety? if p3 is a shared smart pointer object or a copy of a shared smart pointer it points internally to one single reference object. so actually you were using one shared_ptr in all threads. if you reset or assign a new pointer to the shared_ptr, all threads which use the shared variable or a copy of it, automatically would use the new pointer and not the old one. thread-safety does not mean, that you can't make any pointer mistakes, but only that you can be sure that if thread A changes the variable prior to thread B but nearly same time, it is still guaranteed that B sees the new one and not something what is undefined (like the life or death of the Schroedinger cat).

(even when these instances are copies, and share the same reference count underneath.)
with this you can see how it was made. same as with a string class which does reference counting any copy of a shared pointer points to a reference object with a counter. all actions are done with the reference object and not with the 'wrapper' variables you were using.

Sara
0
Exploring SQL Server 2016: Fundamentals

Learn the fundamentals of Microsoft SQL Server, a relational database management system that stores and retrieves data when requested by other software applications.

mrwad99Author Commented:
OK, this is not my code, it is code from http://www.boost.org/doc/libs/1_49_0/libs/smart_ptr/shared_ptr.htm#ThreadSafety

if p3 is a shared smart pointer object or a copy of a shared smart pointer it points internally to one single reference object. so actually you were using one shared_ptr in all threads.

This is what I am not sure about.  The first example I posted, which the boost documentation states is "undefined, multiple writes" accesses the same object (p3) in two threads.

The second example I posted, which is apparently OK ("OK, writes p2"), writes to p and p2 at the same time.  I am assuming that p2 is a copy of p, otherwise the statement I quoted "...even when these instances are copies, and share the same reference count underneath.)" is false.  Furthermore, if p and p2 are really different objects, i.e. they have a separate underlying pointer and not related in any way at all, then this example doesn't prove anything useful whatsoever.

But as I have said, I have looked at the code for reset() and there is no locking mechanism present at all!  Hence my confusion!
0
sarabandeCommented:
shared_ptr objects offer the same level of thread safety as built-in types.
ok. i understand now your concerns since from this statement the shared_ptr is NOT thread-safe or more precise it is not safer than a pointer. if you read from it from multiple threads you can be sure to get a valid object. but if any of the threads changes the managed pointer object, you may encounter the same issues as for a shared built-in type:

(1) another thread is still using the old object since the compiler has optimized the code and used a tempory and was not aware that the pointer has changed (for this probably the volatile keyword would help if it is intended that the variables could change).
(2) a second thread uses the old pointer object while it was freed. probably a crash.
(3) a second thread changes the shared_ptr nearly same time (don't believe that this is a likely scenario).

but the main flaw is that when writing to a thread-shared shared_ptr while being accessed by other threads obviously is not safe, beside you would protect the use of the shared_ptr with a mutex.

i personally don't use shared pointers. i prefer to use one singleton pointer for any shared object. if you put all these pointers to one container, you easily could guarantee thread-safety by protecting the container and each element by one mutex or critical section.

Sara
0
mrwad99Author Commented:
Oh dear.

shared_ptr objects offer the same level of thread safety as built-in types.

But that completely contradicts the next sentence

"Different shared_ptr instances can be "written to" (accessed using mutable operations such as operator= or reset) simultaneosly by multiple threads (even when these instances are copies, and share the same reference count underneath.)"

Isn't that what example 2 from the same link is intended to show - two copies of a shared_ptr object that both point to the same underlying raw pointer?  But as I have said there is no protection around the reset() method at all in the source code!

//--- Example 2 ---

// thread A
p.reset(new int(1912)); // writes p

// thread B
p2.reset(); // OK, writes p2

Open in new window

0
evilrixSenior Software Engineer (Avast)Commented:
Sorry I got to this late. It's pretty simple really, the same shared pointer cannot be modified/read from different threads at the same time. Modifying/reading different instances of a shared pointer that reference the same resource is fine providing the action doesn't require reference to a instance that might be modified by another thread. It's basically no different than any other data type, you can modify in one thread and read/modify in another. My advice, forget it's a shared pointer and just treat it with the same reverence you would any other data type when it comes to thread safety.
0
mrwad99Author Commented:
Hi Rx!  I hope you are well :)

It's pretty simple really, the same shared pointer cannot be modified/read from different threads at the same time

So I can't do this:

shared_ptr<int> p1(new int(42));

// Thread A
p1.reset(new int(1));

// Thread B
p1.reset(new int(1));

Open in new window



Modifying/reading different instances of a shared pointer that reference the same resource is fine...

OK, so I am assuming I can do this:

shared_ptr<int> p1(new int(42));
shared_ptr<int> p2(p1);

// Thread A
p2.reset(new int(1));

// Thread B
p1.reset(new int(1));

Open in new window


.. because that seems to agree with the line that I have quoted oh so many times...

"Different shared_ptr instances can be "written to" (accessed using mutable operations such as operator= or reset) simultaneosly by multiple threads (even when these instances are copies, and share the same reference count underneath.)"


But as I have said there is no code around reset() that does any locking of any kind, so I don't see how this can possibly be safe!

...providing the action doesn't require reference to a instance that might be modified by another thread.  

I don't understand that last bit at all.  Can you please clarify?
0
sarabandeCommented:
Different shared_ptr instances can be "written to"
probably you have to take the text literally, different is different. and not a copy of the "same" shared pointer what would be consistent with the statement that a copy uses the same internal object and is not 'different'.

principially, it would not be so difficult to make a shared pointer class thread-safe in the way you assumed it was made, though it would mean that a reset would block as long as any other thread has some kind of lock to the current shared pointer. but actually what should this kind of thread-safety be good for? you can't simply change a good pointer by another good pointer on the fly and expect other threads can handle this.

Sara
0
sarabandeCommented:
OK, so I am assuming I can do this:
yes, you can do this because p1 and p2 are independent. but you can't safely use p1 in thread B (beside of read-only access and granted that the p1 wasn't reset again). same applies for p2 in thread a.

Sara
0
mrwad99Author Commented:
I see your point about taking the text literally, and if it wasn't for this part:

"...(even when these instances are copies, and share the same reference count underneath.)"

...I would have on confusion.  But to share the same reference count underneath must mean it is the same underlying raw pointer, or not??
0
sarabandeCommented:
But to share the same reference count underneath must mean it is the same underlying raw pointer, or not??
yes. i think it means more. it means that you can reset p1 and p3 even if p3 is a copy and not the original p2 which created the raw pointer.

nevertheless it is an awful explanation of a simple thing that can also be simply explained as evilrix did in his comment.

Sara
0
evilrixSenior Software Engineer (Avast)Commented:
>> Hi Rx!  I hope you are well :)
Ditto :)

>> So I can't do this:
No, that's modifying the same pointer at the same time in two seperate threads.

>> OK, so I am assuming I can do this:
Yes, that's fine - two different pointers in two different threads

>> But as I have said there is no code around reset() that does any locking of any kind
Well, reset will just decrement its counter and that's done using an interlocked decrement. Once that's done the shared pointer will either no longer point to anything or point to something new. Not sure what else you'd expect to see. How is reset, for example, any different than assignment/copying? As long as the instance is only being modified in one thread then it's safe.

>> providing the action doesn't require reference to a instance that might be modified by another thread
In other words, you can modify A as long as A don't depend on B not being modified during the time it takes to modify A. In other words, watch out for destructors being called that on deleted opjects that might be getting accessed at the same time in another thread.

>> probably you have to take the text literally, different is different. and not a copy of the "same" shared pointer
No, just different instances - they can point to the same object. What they point to is irrelevant. Forget they contain pointers, they are just objects like any other and the have the same thread safely requirements as any other object. You can't modify the same instance of a shared pointer object in two threads at the same time, you can modify two different instances even if they point to the same thing. A shared pointer only has two things to be managed internally, the pointer to which it points and a (normally pointer to a) counter. The counter is interlock modified. The so taking copies and so on is fine because the counter can be safely modified cross-thread. Changing what the pointer points to is NOT thread safe. Hence, you can't call reset on the same instance of the same object in two threads at the same time. You can call reset on two different instances because it's just two different internal pointers being nullified.

>> But to share the same reference count underneath must mean it is the same underlying raw pointer, or not??
The reference count is normally implemented as a pointer to a integer type value. Each shared pointer that points to the same object also points to the same counter. The counter is modifiled using interlocked functions. The pointers belong to each class and so one shared pointers internal pointers are not shared by any other. The only thing they share in common is what what they point to.

Again, all I can say is don't worry about what it is - it's just a C++ object that has exactly the same thread safely concerns as any other object. There is nothing magical about a shared pointer in that respect.

Take a look at my article about how reference counted smart pointers are implemented (it's a very simplified example but maybe it will help you understand why there's nothing special about shared pointers when it comes to thread safety).

http://www.experts-exchange.com/articles/1959/C-Smart-pointers.html
0
mrwad99Author Commented:
Thanks Sara.

RX - nice to hear from you again :)

I've read your smart pointer article a long time ago, it was useful, so thanks :)

Both:

Yes, that's fine - two different pointers in two different threads
I've just spent the last hour or so going through the code in Boost's reset() (yep, I'm at the cutting edge of self-entertainment, evidently :)) and I don't think it is thread-safe.

The way reset seems to work is that it constructs a temporary shared_ptr object, then swaps its "attribute" with those of the callee.  Consider my experimental code:

boost::shared_ptr<int> pS(new int(100));
boost::shared_ptr<int> pS2(pS);

pS.reset(new int(10));

// boost\smart_ptr\shared_ptr.hpp
template<class Y> void reset(Y * p) // Y must be complete
{
	BOOST_ASSERT(p == 0 || p != px); // catch self-reset errors
	this_type(p).swap(*this);
}

// boost\smart_ptr\shared_ptr.hpp
void swap(shared_ptr<T> & other) // never throws
{
	std::swap(px, other.px);
	pn.swap(other.pn);
}

Open in new window


Here, reset() will construct a temp. shared_ptr with the pointer to 10 and a ref count of 1.  swap() then swaps those attributes with those of pS, so the temp. object ends up with a ref count of 2 and a pointer to 100, and pS will end up with a ref count of 1 and a pointer to 10.  The temp. object goes out of scope, so we end up with pS having a ref count of 1 instead of 2.  Job done.

The issue is in swap; as far as I can see, there is no locking around this, so I can't see how this can ever be safe:

shared_ptr<int> p1(new int(42));
shared_ptr<int> p2(p1);

// Thread A
p2.reset(new int(1));

// Thread B
p1.reset(new int(1));

Open in new window


If you have the time, or inclination, I would be ever so grateful if you could take a look at this and cast your experienced eyes over what is most likely a mistake on my part...
0
evilrixSenior Software Engineer (Avast)Commented:
Look at this this was, this is how it works

You have shared pointer A1
swap creates A2 and copies A1 (all in the same thread)
A1 receives the original values of A2 (nullptr and 0 count)
A2 then goes out of scope (destructor decrements count)

All this happens in the same thread, so no worries. As long as another thread doesn't try to change A1 whilst it receives the nullptr and 0 count then it's perfectly safe. Only if A1 is being used in another thread is this unsafe. The fact that a temporary pointer is created that is intialised with a pointer to the object and temporarily increments the reference counts doesn't change that. Why do you think it should?

BTW: The swap with null object is quite a common design idiom in Boost. It means you don't have to write a special function to do the cleanup - the destructor takes care of it for you. But, ultimately, this isn't really any different than just having the reset function decrement the count and nulling the pointer. Providing it's not being used in another thread it's safe.

...or, have I misunderstood your question?
0
sarabandeCommented:
perhaps this way:

a reset on p1 has no effect on p2 because the reference count would be decremented and nothing was released.

same happens if p2 was reset prior to p1. p2 no longer manages the same pointer together with p1 but has its own pointer then.

hence the reset operations are thread-safe themselves granted the decrementation happens in a thread-safe manner.

Sara
0
evilrixSenior Software Engineer (Avast)Commented:
Hahaha. What Sara said! Way clearer than my 1/2 asleep ramblings ;)
0
mrwad99Author Commented:
OK, sorry about the delay here.

Consider the following code:

boost::shared_ptr<int> p1(new int(100));
boost::shared_ptr<int> p2(p1);

void ThreadFunc(void* pP)
{
	p2.reset(new int(2));
}

int main(int argc, char* argv[])
{
	_beginthread( ThreadFunc, 0, 0);
	p1.reset(new int(1));
}

Open in new window


Now, I used Visual Studio's thread pausing facility here to pause threads.  I got both threads sitting on the swap() call which is made from within reset():

void swap(shared_ptr<T> & other) // never throws
{
	std::swap(px, other.px);
	pn.swap(other.pn);
}

Open in new window

which has been called from

template<class Y> void reset(Y * p) // Y must be complete
{
	BOOST_ASSERT(p == 0 || p != px); // catch self-reset errors
	this_type(p).swap(*this);
}

Open in new window


Hovering over px in both threads reveals the same memory address: that of the integer 100.

We are writing to the same address (that of px) from two different threads, in an operation which is not atomic without synchronisation.  

Yet the documentation seems to state this is possible (yes, its that much quoted passage again!)

"Different shared_ptr instances can be "written to" (accessed using mutable operations such as operator= or reset) simultaneously by multiple threads (even when these instances are copies, and share the same reference count underneath.)"

What am I missing?
0
evilrixSenior Software Engineer (Avast)Commented:
You seem to be confusing modifying  the pointer with what it points to.
0
mrwad99Author Commented:
All I can see is that the documentation doesn't appear to be accurate.  Can you please clarify?

And please, send me a virtual slap if I am having a dumb moment...
0
evilrixSenior Software Engineer (Avast)Commented:
px  is a pointer, each thread has its own px. Changing on in thread A does not affect the copy in thread B. That is all swap is doing, copying the value in px, not what it points to. Since each thread has its own px how does modifying one affect the other?
0
evilrixSenior Software Engineer (Avast)Commented:
Look at it another way, what if each thread contained an int, both initialised to 100. Changing the int in one thread to point to 99 in no way affects the other int that continues to point to 100. Same here, you are changing what the pointer is pointing too not the object being pointed at (which is what the treads are sharing). Since each thread has its own pointer object modifying one pointer does not affect the other.
0
evilrixSenior Software Engineer (Avast)Commented:
Does this visualisation help?

=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
......................
 MAIN THREAD
 P1--+
     |
     |
.....|................
     |
     v
    int(100)
     ^
     |   
.....|................
     |
     |
 P2--+
 WORKER THREAD
......................

=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
// Now some changes happen but each pointer is only modified in one thread
// and the fact they both used to point to the same object doesn't change the
// fact they are still unique copies of pointer data types to each thread. A
// pointer is not special, it is just a data type (just like an int of a float)
// and just because two different pointers in two different threads both point
// at the same object doesn't mean changing the contents of one pointer in one
// thread will have any visibility in the other thread. In the case of a smart
// pointer the only thing the other thread sees change is the reference count
// but that is modified using an interlocked function, which is thread safe.
p1.reset(new int(1)); // main thread

p2.reset(new int(2)); // worker thread
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

.....................
MAIN THREAD
P1---------> int(1) // P1 is only modified in main thread (to point to int(1))


.....................



    delete int(100) // never modified, but is deleted no no longer referenced


.....................


P2---------> int(2) // P2 is only modified in worker thread (to point to int(2))
WORKER THREAD
.....................
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

Open in new window

0
evilrixSenior Software Engineer (Avast)Commented:
Or some simplified code:

 
#include <memory>
#include <thread>
#include <functional>

using namespace std;

int main()
{
	// make_shared is a convenience function for creating shared_ptr types
	auto p1 = make_shared<int>(100);
	auto p2 = p1; // incr pn & copy p1.px into p2.px; both now point at int(100)
	
	auto t = thread(
		// this is just a lambda function, accepting p2 by reference
		[&p2] {
			p2 = make_shared<int>(2); // changing what p2 points at
			}
		);
		
	p1 = make_shared<int>(1); // changing what p1 points at
	
	// Neither p1 nor p2 are modified at the same time in both threads. The
	// object they both pointed *is* shared between threads but it is never
	// modified at any point so no race condition has occurred.
	
	t.join(); // wait for thread convergence
}

Open in new window

0
mrwad99Author Commented:
OK for ease of future searchers I summarize the answers to my two questions here.

Thanks very much Rx and Sara for having the patience to deal with my clear lack of understanding, which has now been resolved!

1) It seems the way to pass a shared_ptr to a thread is to use std::thread, as this copies the arguments, hence we avoid the problem I outlined with _beginthreadex about the shared_ptr going out of scope before the thread starts.

2) As has been extensively discussed, the reset() function will only actually adjust the pX and pN members of each shared_ptr; it won't adjust what they actually point to.  Hence there is no need to locking.  

As Rx points out, we cannot adjust the same shared_ptr object in multiple threads (which my example doesn't do, it uses two shared_ptrs that happen to point at the same data, not the same shared_ptr!)
0

Experts Exchange Solution brought to you by

Your issues matter to us.

Facing a tech roadblock? Get the help and guidance you need from experienced professionals who care. Ask your question anytime, anywhere, with no hassle.

Start your 7-day free trial
evilrixSenior Software Engineer (Avast)Commented:
1. Well, it doesn't matter if a copy is taken or the scope of the variable is persisted throughout the life of the thread using it by making it global. Personally, I prefer passing a copy of the object to the thread because the thread then owes it. Boost thread also works exactly the same way. In fact std::thread is modelled from boost::thread.

2. Bingo :)

This is one of those one's that is a lot harder to explain than it really should be but (hopefully) once you get it the reason it's thread safe should be quite obvious (not to say getting to that point is straight-forward by any means).

I hope Sara and I were able to help your understanding but, as always, you know you are welcome to post back here with related questions if anything is still unclear.

All the best.
0
mrwad99Author Commented:
Thanks Rx.  The pieces fell into place when I started scribbling some diagrams down.  And thanks again for the reminder that I can come back with follow ups; you know I will I have issues :)

See you in a future question!
0
evilrixSenior Software Engineer (Avast)Commented:
Hahaha. Any time.

You know I'll be honest, I'm really not very good with pointers. I get confused beyond 1 level of dereferencing. Drawing a diagram of what you believe to be happening is, by far, the best advice. Personally, I find being able to visualise what is happening is the best way to understand.

As always, I'm very happy we could help and your follow-up questions are always welcome.
0
sarabandeCommented:
all the best from me too.

my résumé is that is that it is fine when a container is thread-safe regarding copies, but actually this is only the most least part of making the container thread-safe with regards to making the access to the data it contains thread-safe as well. from that point of view it doesn't make any sense to use shared_ptr objects managing the same raw pointer in two threads beside for read-only purposes.

Sara
0
mrwad99Author Commented:
Thanks for that Sara.

Best wishes to you both :)
0
mrwad99Author Commented:
Summary of answers given to aid future searches.
0
It's more than this solution.Get answers and train to solve all your tech problems - anytime, anywhere.Try it for free Edge Out The Competitionfor your dream job with proven skills and certifications.Get started today Stand Outas the employee with proven skills.Start learning today for free Move Your Career Forwardwith certification training in the latest technologies.Start your trial today
C++

From novice to tech pro — start learning today.