Sometimes I need a reference counting smart pointer that should not be nullptr. For example, I want to share a very heavy object without copying. And it's more convenient to place it on the heap. So I could use a shared_ptr. Also, I don't need to assign nullptr to this pointer or reassign it. So I don't want to check for nullptr in code that uses it. I think it would be useful to have a pointer that cannot be nullptr and cannot be reassigned.
I wrote a small prototype that has this features.
#include <iostream>
#include <string>
#include <memory>
#include <exception>
#include <thread>
using namespace std;
// Box cannot hold nullptr
template<class T>
class Box
{
public:
template <class... Args>
static Box<T> create(Args&&... args)
{
return Box<T>(make_shared<T>(std::forward<Args>(args)...));
}
T * operator-> () const
{
return ptr.operator -> ();
}
private:
Box(const shared_ptr<T> & ptr)
: ptr(ptr)
{}
shared_ptr<T> ptr;
};
struct Test
{
Test(int i, double d, const string & s)
: i(i), d(d), s(s)
{
cout << "Test ctor" << endl;
}
~Test()
{
cout << "Test dtor" << endl;
}
Test(const Test & test) = delete;
Test(Test && test) = delete;
Test & operator = (const Test & test) = delete;
Test & operator = (Test && test) = delete;
int i;
double d;
string s;
};
void print(const Box<Test> & test)
{
// there is no need to check for nullptr
cout << test->i << " " << test->d << " " << test->s << endl;
}
void twice(Box<Test> test)
{
// there is no need to check for nullptr
test->i *= 2;
test->d *= 2;
test->s += test->s;
}
/*
stdout:
Test ctor
42 3.14 hello
84 6.28 hellohello
Test dtor
*/
int main()
{
Box<Test> test = Box<Test>::create(42, 3.14, "hello");
print(test);
thread t(twice, test);
t.join();
print(test);
}
Is this code correct? And is this a good approach?
Update: It looks like I found a ready solution https://github.com/dropbox/nn
Related discussions:
1) https://groups.google.com/a/isocpp.org/forum/#!topic/std-proposals/5TPClBA1fs8
2) http://boost.2283326.n4.nabble.com/Why-no-non-null-smart-pointers-td2642959.html
3) https://lists.boost.org/Archives/boost/2013/10/206732.php
2 Answers 2
Well, in principle having non-nullable pointers, smart or not, is a good thing. It's just a shame that nullability is not an optional non-default trait (not only for pointers, but for all types; for numbers non-zeroability might be an interesting companion-trait, which incidentally makes the former more efficient).
Your implementation of it leaves something to be desired though, because you completely block conversion from "normal" nullable pointers to your non-nullable one and back. That severely limits your types utility.
Also, one expects a lot more functionality from a pointer-like type, like more conversions, more operators, and the rest of the smart-pointer-interface, at least as far as compatible with the design-goals.
Now let's look at the code of your prototype:
Separate your new smart-pointer from the test-code, and make it a self-contained header-only-library.
Never use
using namespace std;
in an include-file, not that it is much better in a source-file. Read "Why is "using namespace std;" considered bad practice? " for the reasons.As said above, you need loads more accessors, and all the other functons, member and free, which comprise the interface for a smart-pointer.
If you use private inheritance, you have an easier time passing through the needed interface with
using
than writing it from scratch.
And the code of your test:
Only explicitly flush a stream if you really need to. Doing so frivolously is quote costly.
If you want to output a single character to a stream, consider doing so explicitly instead of with a length-1 C-string. It's potentially slightly more efficient.
-
\$\begingroup\$ Thanks for the review! Of course, this code is far from being production ready. I intentionally didn't split it into several files, to keep it as small as possible and leave the opportunity to play with it on sites like www.cpp.sh. I agree that the interface should be extended, as you said. On the other hand, my class looks more like a reference, not a pointer. Something like the std::reference_wrapper with ownership semantics. So maybe I'll completely rethink and rewrite it. But I've never seen anyone use such approach. \$\endgroup\$bloom256– bloom2562018年04月01日 18:48:30 +00:00Commented Apr 1, 2018 at 18:48
I wrote, as a comment, that it seemed like one could use a reference instead. This answer shows what that might look like in a little more detail.
Use references when pointers can't be nullptr
As you know a reference must point to an actual object and can't be nullptr
, so it seems that everywhere you have Box<Test>
, one could simply substitute Test
:
void print(const Test & test)
{
// there is no need to check for nullptr
std::cout << test.i << " " << test.d << " " << test.s << std::endl;
}
void twice(Test& test)
{
// there is no need to check for nullptr
test.i *= 2;
test.d *= 2;
test.s += test.s;
}
Use std::ref
where needed
Because we are now using a reference, as an argument to twice()
, we need to make sure that we use std::ref
when creating the thread:
int main()
{
Test test{42, 3.14, "hello"};
print(test);
std::thread t(twice, std::ref(test));
t.join();
print(test);
}
Use the "Curiously Recurring Template Pattern" where appropriate
The Curiously Recurring Template Pattern is a handy way to add "decorations" such as your constructor and destructor announcements. Here's one way to do that without cluttering up the code for the actual concrete class:
template <typename T>
struct Announcer {
Announcer() { std::cout << typeid(T).name() << " ctor\n"; }
~Announcer() { std::cout << typeid(T).name() << " dtor\n"; }
};
This requires #include <typeinfo>
and perhaps a little explanation. The typeid(T).name()
returns an implementation-defined name for the type T
. Using gcc, it's the mangled typename of the class. Here's how to use it:
struct Test : Announcer<Test>
{
Test(int i, double d, const std::string & s)
: i(i), d(d), s(s) {}
Test(const Test & test) = delete;
Test(Test && test) = delete;
Test & operator = (const Test & test) = delete;
Test & operator = (Test && test) = delete;
int i;
double d;
std::string s;
};
Note that the Test
code is now quite plain and otherwise unaltered from your original. However, the key is in this line:
struct Test : Announcer<Test>
This often seems, even to experienced C++ programmers, as something that shouldn't actually compile, but it does and it's quite handy. What we now have is a Test
class that is based on the templated base class Announcer<Test>
. The effect is very similar to your original code:
4Test ctor
42 3.14 hello
84 6.28 hellohello
4Test dtor
The 4Test
in the output is the mangled name. With my Linux system, one can translate that into the actual code syntax by piping the output to c++filt -t
. Try putting Test
into a namespace and you'll see that the namespace becomes part of the reported name.
It's still not entirely clear to me what problem you're trying to solve in your real code, but I hope this has given you at least something else to think about.
-
1\$\begingroup\$ A reference has the same ownership as a raw pointer: None at all. And it looks like shared ownership is wanted. \$\endgroup\$Deduplicator– Deduplicator2018年04月01日 21:40:24 +00:00Commented Apr 1, 2018 at 21:40
-
\$\begingroup\$ @Deduplicator: it wasn't clear to me from the question that shared ownership is actually needed. For that, we'd need a little more context, hence the last paragraph in my answer. Maybe the goal is automatic garbage collection? I don't know. \$\endgroup\$Edward– Edward2018年04月01日 21:43:32 +00:00Commented Apr 1, 2018 at 21:43
-
\$\begingroup\$ In the test-code, it isn't really needed. Yes, the test is far from complete. \$\endgroup\$Deduplicator– Deduplicator2018年04月01日 21:48:10 +00:00Commented Apr 1, 2018 at 21:48
-
1\$\begingroup\$ Thanks for the review. Yes, the question wasn't very accurate. I like references. But I also need ownership semantics. We have std::optional. In most cases, no one needs to assign nullptr to a smart pointer. So, is it useful to have a smart pointer class with ownership semantics, that can't be nullptr? You can call it smart reference, something like std::reference_wrapper with ownership semantics. That's what I'm trying to figure. \$\endgroup\$bloom256– bloom2562018年04月01日 22:57:57 +00:00Commented Apr 1, 2018 at 22:57
-
\$\begingroup\$ I don't fault the question, really. It's just that the particular use case isn't apparent and I haven't, in my experience, encountered a reason to try to invent such a thing. So I'm a bit puzzled as to what's the goal. \$\endgroup\$Edward– Edward2018年04月01日 23:19:21 +00:00Commented Apr 1, 2018 at 23:19
nullptr
sounds like the very definition of a reference. Why not just use references? \$\endgroup\$ptr.operator -> ()
overptr->get()
? \$\endgroup\$