6
\$\begingroup\$

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

\$\endgroup\$
  • 2
    \$\begingroup\$ A "pointer" that can't be nullptr sounds like the very definition of a reference. Why not just use references? \$\endgroup\$ – Edward Apr 1 '18 at 14:55
  • \$\begingroup\$ What is the advantage of ptr.operator -> () over ptr->get()? \$\endgroup\$ – Cris Luengo Apr 1 '18 at 15:52
  • \$\begingroup\$ @Cris Luengo There is no advantage. \$\endgroup\$ – bloom256 Apr 1 '18 at 18:50
  • \$\begingroup\$ @Edward You're right. It's like a reference with ownership semantics. Why don't we have smart references in c++? \$\endgroup\$ – bloom256 Apr 1 '18 at 18:54
6
\$\begingroup\$

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:

  1. Separate your new smart-pointer from the test-code, and make it a self-contained header-only-library.

  2. 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.

  3. As said above, you need loads more accessors, and all the other functons, member and free, which comprise the interface for a smart-pointer.

  4. 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:

  1. Only explicitly flush a stream if you really need to. Doing so frivolously is quote costly.

  2. 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.

| improve this answer | |
\$\endgroup\$
  • \$\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 Apr 1 '18 at 18:48
2
\$\begingroup\$

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.

| improve this answer | |
\$\endgroup\$
  • 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 Apr 1 '18 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 Apr 1 '18 at 21:43
  • \$\begingroup\$ In the test-code, it isn't really needed. Yes, the test is far from complete. \$\endgroup\$ – Deduplicator Apr 1 '18 at 21:48
  • \$\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 Apr 1 '18 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 Apr 1 '18 at 23:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.