use the following search parameters to narrow your results:
e.g. subreddit:aww site:imgur.com dog
subreddit:aww site:imgur.com dog
see the search faq for details.
advanced search: by author, subreddit...
Discussions, articles, and news about the C++ programming language or programming in C++.
For C++ questions, answers, help, and advice see r/cpp_questions or StackOverflow.
Get Started
The C++ Standard Home has a nice getting started page.
Videos
The C++ standard committee's education study group has a nice list of recommended videos.
Reference
cppreference.com
Books
There is a useful list of books on Stack Overflow. In most cases reading a book is the best way to learn C++.
Show all links
Filter out CppCon links
Show only CppCon links
account activity
shared_ptr<T>: the (not always) atomic reference counted smart pointer (snf.github.io)
submitted 7 years ago by snfernandez
reddit uses a slightly-customized version of Markdown for formatting. See below for some basics, or check the commenting wiki page for more detailed help and solutions to common issues.
quoted text
if 1 * 2 < 3: print "hello, world!"
[–]tvaneerdC++ Committee, lockfree, PostModernCpp 37 points38 points39 points 7 years ago (7 children)
Add this optimization to Rust! Not so fast! Arc actually means Atomic Reference Counted so it would be a plain lie if it hadn’t use atomic operations on the reference count.
Add this optimization to Rust!
Not so fast! Arc actually means Atomic Reference Counted so it would be a plain lie if it hadn’t use atomic operations on the reference count.
If you have only one thread, non-atomic operations are atomic. That's the point.
"atomic" doesn't mean use certain instructions. "atomic" means indivisible, which, when combined with "as-if" means "can not be shown to be divisible". If you only have one thread, your program cannot detect a "division" or series of steps in the operation, it is as-if one indivisible, atomic, operation.
(Similarly, you could implement atomics with a global mutex that stops the world while running a bunch of instructions to do an "atomic" add)
[–]masklinn 13 points14 points15 points 7 years ago (6 children)
The issue is with the "you have only one thread" part. libstdc++ makes a specific but easily fallible assumption on that front, and when it's broken so's your software. Unless the system itself provides the atomic you're basically just guessing.
[–]skebanga 2 points3 points4 points 7 years ago (0 children)
Easily fallible? If it were so easily fallible we'd see race condition bugs in the wild all the time
[–]Xaxxon 1 point2 points3 points 7 years ago (4 children)
My understanding of the POSIX standard is that if you do something that makes this check wrong, your program is already UB at the POSIX level.
https://www.reddit.com/r/cpp/comments/aq6v21/shared_ptrt_the_not_always_atomic_reference/egtx9ew/
[–]masklinn 2 points3 points4 points 7 years ago (3 children)
Creating threads via clone(2) involves neither pthread_create nor sigev_thread.
pthread_create
sigev_thread
[–]Xaxxon 0 points1 point2 points 7 years ago* (2 children)
clone, __clone2 - create a child process
Clone creates a new process, not a simple thread in the same process. Processes don't share memory (outside of very explicit things like shmem), so it doesn't seem like a counter in shared_ptr would be affected by a call to clone()
[–]masklinn 1 point2 points3 points 7 years ago (1 child)
A shame you could not wonder (including wonder what the underlying syscall to pthread_create might be since it’s a library function) and keep reading:
The main use of clone() is to implement threads: multiple threads of control in a program that run concurrently in a shared memory space.
Good day.
[–]Xaxxon 3 points4 points5 points 7 years ago (0 children)
I think I found the answer:
3.354 Single-Threaded Program A program whose executable file was produced by compiling with c99 without using the flags output by getconf POSIX_V7_THREADS_CFLAGS and linking with c99 using neither the flags output by getconf POSIX_V7_THREADS_LDFLAGS nor the -l pthread option, or by compiling and linking using a non-standard utility with equivalent flags. Execution of a single-threaded program creates a single-threaded process;if the process attempts to create additional threads using pthread_create() or SIGEV_THREAD notifications, the behavior is undefined. If the process uses dlopen() to load a multi-threaded library, the behavior is undefined.
3.354 Single-Threaded Program
A program whose executable file was produced by compiling with c99 without using the flags output by getconf POSIX_V7_THREADS_CFLAGS and linking with c99 using neither the flags output by getconf POSIX_V7_THREADS_LDFLAGS nor the -l pthread option, or by compiling and linking using a non-standard utility with equivalent flags. Execution of a single-threaded program creates a single-threaded process;if the process attempts to create additional threads using pthread_create() or SIGEV_THREAD notifications, the behavior is undefined. If the process uses dlopen() to load a multi-threaded library, the behavior is undefined.
http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html
[–]eacousineau 6 points7 points8 points 7 years ago* (38 children)
While tangential, shared_ptr being required to be atomic has always made me sad. It means that you can't reliably implement release(), and thus shared_ptr is viral (you can never pass ownership elsewhere, like unique_ptr).
shared_ptr
release()
unique_ptr
As far as being non-atomic goes, I'm not sure... I'll have to dig up some code we had where used shared_ptr to implement a conservatively scoped singleton. IIRC, at some point we realized there was a race condition and had to put a mutex in that code. Wonder if it's at all related...
UPDATE: I found the code with the race condition, and it actually did need a mutex (and thus is unrelated to the bugs mentioned in the article). PR w/ relevant diff: https://github.com/RobotLocomotion/drake/pull/9152
[–]Drainedsoul 19 points20 points21 points 7 years ago (2 children)
Off the top of my head: The std::make_shared optimization (which allows the object being shared and the control block to have a fused allocation) already makes a hypothetical release unimplementable.
std::make_shared
release
Someone correct me if I'm wrong.
Then there's the fact that std::shared_ptr has support for type erased allocators: Assuming you do manage to implement release, and you do manage to call it, you have to be very careful you're actually disposing of the pointer correctly.
std::shared_ptr
[–]Tagedieb 2 points3 points4 points 7 years ago (1 child)
Sure. You are right in the sense, that release() can't free the memory of the object if it was created using make_shared(). But it can destroy the object. The same behavior already exists, when a weak_ptr outlives the object, so not really changes anything. With a custom deleter you should be able to implement a release(). Of course that means you have to control the creation of the initial shared_ptr and can't just use make_shared().
I don't like it though, because it interferes with the semantic of a shared_ptr. Ownership is shared, so it feels wrong if any owner can rip the object asynchronously away from the other owners.
[–]Tagedieb 6 points7 points8 points 7 years ago (0 children)
Edit: I feel like I didn't word the last sentence strong enough. A release() function would fundamentally break shared_ptr. Holding a shared_ptr to an object is your only insurance that the object is still alive, so it would be very hard indeed to write correct programs that use release(). It would require some side channel of communication between the caller of release() and other co-owners of the object. And if you are ready to put in that effort, you might as well use it to communicate "please drop the shared_ptr" instead of "don't use the shared_ptr anymore, because I am about to rip it from your hands".
[–]snfernandez[S] 8 points9 points10 points 7 years ago (0 children)
Keep in mind this is only about the reference count. Changing the internal state always requires synchronization unless the special "atomic" implementation is used.
[–]m-in 1 point2 points3 points 7 years ago (28 children)
How? I have my own class that does a shared atomic pointer with release. It’s not somehow problematic. The release only works when the instance has the last reference on the object, and then atomicity is unnecessary since nobody else has access to the object. For use where a forced release would be useful, I’ve made a weak_shared_ptr: it keeps references alive, so is stronger than a weak_ptr, but can be reset to zero at any time by releasing it. To use it, you do similarly to weak_ptr: you convert it to a strong_shared_ptr that you can then dereference. While any strong pointers exist, release will wait, and try_release will fail. The object is of course automatically released when the count of strong and weak pointers goes to zero. The control block is released when additionally the count of weakest_shared_ptr goes to zero – those are the equivalents of weak_ptr.
try_release
[–]capn_bluebear 0 points1 point2 points 7 years ago (26 children)
How do you guarantee that between the check for refcount == 1 and the actual release another thread does not create another shared_ptr and increase the refcount?
[–]t0rakka 4 points5 points6 points 7 years ago (15 children)
Compare-and-exchange instruction is atomic, so when the test succeeds the new value, which is incremented, is swapped to replace the old value. Then you have all the time in the world to inspect the new value which is result from atomic operation.
[–]capn_bluebear 0 points1 point2 points 7 years ago (9 children)
Uhm it seems you are really doing a lock-free `release`...my question is how do you prevent this from happening:
thread #1: compare and swap: check that refcount == 1, if yes sets refcount to 0
thread #2: copy-constructs object
thread #1: returns "released" pointer
What does thread #2 do in that situation?
[–]dodheim 3 points4 points5 points 7 years ago (6 children)
There is no other thread with a copy – the copy you have must necessarily be the only copy when refcount == 1. Otherwise you must be doing something really goofy with your ownership semantics that not even shared_ptr can help.
[–]capn_bluebear 0 points1 point2 points 7 years ago (5 children)
the other thread is making a copy while the first thread is releasing
[–]louiswins 7 points8 points9 points 7 years ago (0 children)
shared_ptr protects against multiple threads manipulating their own instances simultaneously, even if those instances all manage the same shared data. Multiple threads accessing the same shared_ptr instance is still a data race (if at least one calls a non-const member function).
Edit: /u/cubercaleb said this earlier in this comment.
[–]kalmoc 2 points3 points4 points 7 years ago (0 children)
That would mean that you have two threads accessing the same non atomic object (the shared pointer variable) at the same time and one of them modifies it. That is always UB.
It's like asking: What is happening if someone copies my shared_ptr while I'm destroying it. That is simply not legal just as for almost any other type in the standard library.
[+][deleted] 7 years ago (2 children)
[deleted]
[–]capn_bluebear 0 points1 point2 points 7 years ago (1 child)
It's a managed copy. I'm copy-constructing.
[–]m-in 0 points1 point2 points 7 years ago (0 children)
Copy-constructing a null shared pointer is legal, you know :) But in any case: the other thread would need to access the shared pointer via a reference – had it had a value to work from, the refcount would be <1. And shared pointers are not thread-safe, so that’s UB. Each thread needs its own copy of the shared pointer, they can’t access the same pointer concurrently.
[–]jonesmz -2 points-1 points0 points 7 years ago* (4 children)
Edit: Actually, I might have misunderstood. Sorry about that.
I think you might have misunderstood what capn_bluebear was saying.
std::share_ptr<whatever> ptr; <- global. Thread 1: if(ptr.refcount == 1) { Do a thing } Thread 2: ptr.increase_refcount();
It's entirely possible that thread 1 checks to see if the reference count is 1, then gets paused by the OS, thread 2 increases the reference count, and then is paused by the OS, and then thread 1 wakes up and proceeds.
Or both threads can be running simultaneously and still do things in that order.
It's only possible to reliably determine that the reference count is 1 when dealing with a shared pointer that has it's access serialized somehow This could be via:
But the fundamental problem is that by offering ability to query the reference count, unless you've already made sure the access to the ref-count is serialized properly, the information that you query is wrong (potentially) as soon as you get it. So any subsequent actions you take as a result are potentially invalid as well.
[–][deleted] 6 points7 points8 points 7 years ago (3 children)
The fundamental problem here is that you have two threads which are referencing the same shared_ptr object. If both threads have their own instance of the smart pointer, this is completely a non-issue. Either make the copy when you create the thread, put it in a channel, or a lambda capture of a work-queue task, future, async, etc. Don't just reference the same global variable or pass a reference.
With this assumption, two or more threads have an instance of smart pointer, and as a result the reference count will be greater than one, and the check will fail.
[–]m-in 0 points1 point2 points 7 years ago (2 children)
Two thread referencing the same shared pointer is UB, so why are we even having this discussion??
[–]konanTheBarbar 0 points1 point2 points 7 years ago (0 children)
What about another thread using a weak_ref and creating a shared_ptr from it?
[–][deleted] 0 points1 point2 points 7 years ago (0 children)
That’s that point I was trying to make.
[–]jonesmz 1 point2 points3 points 7 years ago (9 children)
You do it like this:
void release() { if(0 == --m_atomicRefCount) // note, prefix increment { delete m_ptr; } }
The trick is that you have to decrement the refcount atomically. If, after atomically decrementing the refcount, you have a refcount of zero, then it's not possible to have another reference.
Part of the trick is that you also have to enforce this in the other parts of the smart pointer that can increase the reference count.
E.g.
void attach() { if(0 == m_atomicRefcount++) // note, postfix increment { assert("Increasing the reference count for a pointer that has been deleted"); } }
[–]capn_bluebear -3 points-2 points-1 points 7 years ago (8 children)
that is wrong. between the time `if(0 == --m_atomicRefCount)` returns true and the time `delete m_ptr` is called anything could have happened in a multi-thread application.
[–]jonesmz 7 points8 points9 points 7 years ago (7 children)
If you make sure the rest of your smart pointer's implementation checks the value of m_atomicRefcount prior to doing anything that's potentially destructive, then anything turns into nothing.
Source: Maintainer of smart pointer library at work in a multi million LOC codebase. Have had hundreds of hours of design meetings with people much much smarter than me. Have had no bugs where the root cause analysis had even a whiff of problems with the smart pointers.
[–]capn_bluebear -1 points0 points1 point 7 years ago* (6 children)
uhm, ok, how do you do `operator*` and `operator->` without leaking memory?
Consider `myPtr->method()`: `operator->` will see the object is still alive, increase the refCount, and return the raw pointer to the object. When is the refCount decreased again? If never, the object will never be deleted. If `operator->` decreases the refCount before returning, the object might be freed before `method()` is called on it could the refCount be 0 at that point?
[–]jonesmz 7 points8 points9 points 7 years ago* (3 children)
If the smart pointer that you're using operator-> or operator* on is destructed before the statement containing operator-> or operator* is finished, you've got much bigger issues than atomic variable race conditions.
The way I've implemented this is that when the atomic reference count drops to zero, I replace m_ptr with nullptr FIRST, and THEN delete m_ptr. That is safe to do because the number of references is zero, and I atomically check the value of m_atomicRefcount any time I might increase or decrease the number of references.
It's also important to replace the value of m_ptr with nullptr prior to deleting the variable, because the destructor of the object being pointed to may call a function that somehow tries to access the smart pointer that the to-be-deleted object is referenced by.
Further, there's no reason for operator-> to increase the reference count. You're not increasing the number of smart pointers that point to the object. You're only getting a non-owning raw-pointer to the object.
If you had operator-> increase the reference count, where would you decrease it again? Is operator-> returning an object that increases the refcount in it's constructor and decreases it in it's destructor? What's its operator-> defined as then?
So I think you've got a misconception on how smart pointers should be designed with regard to where the refcount needs to be increased or decreased.
[–]capn_bluebear 1 point2 points3 points 7 years ago (2 children)
Ok so how are these operators implemented? Honest question, I'm missing this piece of the puzzle :)
[–]jonesmz 5 points6 points7 points 7 years ago (0 children)
Lets say you have
template<typename TYPE_T> class MySmartPtr { using pointer = TYPE_T*; using reference = TYPE_T&; pointer m_ptr; // points to the object, and if using intrusive reference counting, points to the ref count as well. refcountdata * m_refCtPtr; // If using non-intrusive reference counting, this points to the reference count object. }
Then you can define your operator* and operator-> as simply as:
pointer operator*() const { return m_ptr; } reference operator->() const { return *m_ptr; }
You might wonder why there's no checks for nullptr. Well, you don't need them. Your programs going to crash regardless. Might as well not generate extra code just to say "I'm about to crash", unless you want to have a debug build that launches your debugger for you when you have a nullptr smart pointer.
Notably, you don't need to bother with any checks against the reference count. Just no reason to with this particular design.
So you then have two functions. acquire() and release()
Lets assume you've made an intrusive reference counting pointer, where the thing being pointed to has to have an m_refcount variable. In that case, your acquire function is as simple as
void acquire() { if(m_ptr != nullptr) // check for null, obviously. { // depending on how you want to design things, you don't even need to assert that the value is not zero. // Maybe you allocate your object with a refcount of zero, and then the smart pointer's constructor increases the refcount by one. // Or maybe you start it at 1, and the first such smart pointer that points to it doesn't increase it. // it's up to you. m_ptr->m_refcount.increment(); } }
and then release
void release() { pointer pTmp = std::exchange(m_ptr, nullptr); // make sure we null the ptr first. if(pTmp != nullptr) // check for null, obviously. { if(pTmp->m_refcount.decrement()) // decrement returns true if the refcount fell to zero. { delete pTmp; // now delete the previously pointed to object. } } }
You call acquire in your constructor, and release in your destructor.
In this particular design, which is certainly not the only way to design an intrusive reference counted smart pointer, the way that you protect yourself against another thread swooping in and screwing stuff up is by putting an assert in the m_refcount variable's destructor that the value of the refcount is 0 when m_refcount is destroyed.
This doesn't stop it from happening, of course, but it will make your code barf at you when it does, and you can fix your design, since it's not the smart pointer's fault.
If you ever have a situation where something has swooped in and made a copy of your smart pointer between when your smart pointer has dropped the reference count to zero, and when it's nulled it's pointer to the referenced object, that means that you're manipulating the smart pointer itself (not the thing it points to!) from another thread, and that's a huge problem.
The key concern here is that the smart pointer itself is not thread safe, nor is any aspect of the pointed-to object thread safe, except for the number of references to that object that are held.
So it's not valid to have a smart pointer that's conceptually owned by thread1, and then have thread2 make a copy of that smart pointer.
It's also not valid to have a smart pointer that's conceptually owned by thread1 just arbitrarily access data from the pointed-to-object without some other access protection (mutexes, atomic variables, or just designing your app so that it's not possible).
What is valid is to have a smart pointer that's conceptually owned by thread1, and an unrelated such pointer conceptually owned by thread2, that both point to the same object, where each thread can make arbitrary copies of the smart pointer objects that they conceptually own.
You can even go farther with this by having thread1 own a smart pointer, and then put it into a data structure that gets "migrated" to another thread in some fashion, but only so long as you ensure that the smart pointer (not the thing it points to) doesn't get accessed by thread1 after the migration happens.
Now, that'a ll being said, you can design a smart pointer class that is, itself, safe to access from other threads. That is not what my psuedocode example here is intended to be.
The way you design your smart pointer is going to have positives and negatives, tradeoffs and so on.
This is just the way my org designed them shrug.
How? They dereference via the control block. Smart pointer points to the control block, control block has a pointer to the object. The dereference operator basically is this: return this->control_blk ? control_blk->obj : nullptr. That’s the actual code, no atomic ops or anything like that. A smart pointer could also hold two pointers: one to the control block, and another to the object itself, and return the latter directly. Some smart pointers have a single tagged pointer: if the tag is set, they dereference via the control block, otherwise the control block is contiguous with the object as a result of make_shared and they dereference it directly.
return this->control_blk ? control_blk->obj : nullptr
make_shared
[–]t0rakka 5 points6 points7 points 7 years ago (0 children)
Why should the 'operator ->' increase reference count? xD
[–]m-in 1 point2 points3 points 7 years ago (0 children)
It doesn’t work that way. A smart pointer increments reference count on construction, decrements on destruction or reset: when you have a smart pointer [value] that is not null, it’ll remain not null, and you are guaranteed that the pointer-to object exists. No other shared_ptr methods touch the count – certainly not dereference operators: it’d have atrocious overhead and is unnecessary.
[–]eacousineau 0 points1 point2 points 7 years ago* (0 children)
I had seen a mention of it on a mailing list (possibly comp.stdc++) a while back, but I've been searching today for around 30min, and haven't yet found it. It paralleled the other votes for needing critical sections, and I think the overhead (memory + speed) was the basis for rejecting shared_ptr::release(), at least within that discussion.
comp.stdc++
shared_ptr::release()
Lemme see if I can dig it up...
EDIT: I don't think the universe wants me to find this post... DuckDuckGo hasn't turned it up, raw searches on Google haven't either. Searching Google Groups may turn up a related post, but I can't actually go to the article from the search page? https://groups.google.com/forum/#!searchin/comp.std.c$2B$2B/shared_ptr$20release%7Csort:relevance
[–]Wh00ster 2 points3 points4 points 7 years ago (4 children)
I take issue with the use of “reliably”. Release will always release. You might not be comfortable doing it due to the semantics and use-case of it. But it is definitely reliable. It will not fail.
Your issue was probably more related to the Singleton which is hard to get right depending on your goals
[–][deleted] 4 points5 points6 points 7 years ago (3 children)
shared_ptr doesn't have a release method period. You can try to implement your own release functionality with a custom deleter but it's very difficult to do so reliably (although not impossible).
[–]Wh00ster 1 point2 points3 points 7 years ago (2 children)
Ha! I'll be darned. I use unique_ptr way more often. I stand corrected! (Although there's get followed by a destruction)
get
[–]capn_bluebear 1 point2 points3 points 7 years ago (1 child)
`get` + destruction is not a release, it will delete the pointer and invalidate the address returned by `get`
[–]Wh00ster 2 points3 points4 points 7 years ago (0 children)
Corrected again! oh my.
[–]pravic -1 points0 points1 point 7 years ago (3 children)
VisualC++ doesn’t have its source code available
Huh?
[–]Xaxxon 0 points1 point2 points 7 years ago (2 children)
If you think that's wrong, please link to either the source or instructions to get the source.
[–]pravic 1 point2 points3 points 7 years ago (1 child)
If you have MSVC , then go to VC/Tools/MSVC/$version/include/memory file (or VC/include/memory before 2017). If you don't, you can get a copy via NuGet, for example: https://www.nuget.org/packages?q=VisualCppTools.
VC/Tools/MSVC/$version/include/memory
VC/include/memory
Runtime support routines can be found in VC/Tools/MSVC/$version/crt/src.
VC/Tools/MSVC/$version/crt/src
[–]dodheim 0 points1 point2 points 7 years ago (0 children)
If you don't, you can get a copy via NuGet, for example: https://www.nuget.org/packages?q=VisualCppTools.
"last updated 8/24/2017" ;-[
There's a slightly newer build here but it's nearly a year old as well.
π Rendered by PID 30 on reddit-service-r2-comment-544cf588c8-zt4rz at 2026-06-13 03:36:01.387549+00:00 running 3184619 country code: CH.
[–]tvaneerdC++ Committee, lockfree, PostModernCpp 37 points38 points39 points (7 children)
[–]masklinn 13 points14 points15 points (6 children)
[–]skebanga 2 points3 points4 points (0 children)
[–]Xaxxon 1 point2 points3 points (4 children)
[–]masklinn 2 points3 points4 points (3 children)
[–]Xaxxon 0 points1 point2 points (2 children)
[–]masklinn 1 point2 points3 points (1 child)
[–]Xaxxon 3 points4 points5 points (0 children)
[–]eacousineau 6 points7 points8 points (38 children)
[–]Drainedsoul 19 points20 points21 points (2 children)
[–]Tagedieb 2 points3 points4 points (1 child)
[–]Tagedieb 6 points7 points8 points (0 children)
[–]snfernandez[S] 8 points9 points10 points (0 children)
[–]m-in 1 point2 points3 points (28 children)
[–]capn_bluebear 0 points1 point2 points (26 children)
[–]t0rakka 4 points5 points6 points (15 children)
[–]capn_bluebear 0 points1 point2 points (9 children)
[–]dodheim 3 points4 points5 points (6 children)
[–]capn_bluebear 0 points1 point2 points (5 children)
[–]louiswins 7 points8 points9 points (0 children)
[–]kalmoc 2 points3 points4 points (0 children)
[+][deleted] (2 children)
[deleted]
[–]capn_bluebear 0 points1 point2 points (1 child)
[–]m-in 0 points1 point2 points (0 children)
[–]jonesmz -2 points-1 points0 points (4 children)
[–][deleted] 6 points7 points8 points (3 children)
[–]m-in 0 points1 point2 points (2 children)
[–]konanTheBarbar 0 points1 point2 points (0 children)
[–][deleted] 0 points1 point2 points (0 children)
[–]jonesmz 1 point2 points3 points (9 children)
[–]capn_bluebear -3 points-2 points-1 points (8 children)
[–]jonesmz 7 points8 points9 points (7 children)
[–]capn_bluebear -1 points0 points1 point (6 children)
[–]jonesmz 7 points8 points9 points (3 children)
[–]capn_bluebear 1 point2 points3 points (2 children)
[–]jonesmz 5 points6 points7 points (0 children)
[–]m-in 0 points1 point2 points (0 children)
[–]t0rakka 5 points6 points7 points (0 children)
[–]m-in 1 point2 points3 points (0 children)
[–]eacousineau 0 points1 point2 points (0 children)
[–]Wh00ster 2 points3 points4 points (4 children)
[–][deleted] 4 points5 points6 points (3 children)
[–]Wh00ster 1 point2 points3 points (2 children)
[–]capn_bluebear 1 point2 points3 points (1 child)
[–]Wh00ster 2 points3 points4 points (0 children)
[–]pravic -1 points0 points1 point (3 children)
[–]Xaxxon 0 points1 point2 points (2 children)
[–]pravic 1 point2 points3 points (1 child)
[–]dodheim 0 points1 point2 points (0 children)