6

Consider this snippet:

class Foo
{
    int m_fileDescriptor;
public:
    Bar transformIntoBar()
    {
        Bar bar(m_fileDescriptor);
        m_fileDescriptor = -1;
        return bar;
    }
};

As you can see, once Foo "transforms" into Bar, its m_fileDescriptor becomes -1, which renders the entire object invalid.

The problem's that users of the class have no way of telling that's going to happen.

One solution would be to declare a friend make_bar() function

friend Bar make_bar(Foo f);

and make Foo move-only (which is supposed to happen anyway, I skipped it in the example for brevity). This way, the user has to be aware of the fact, that their Foo object is going to be moved from and therefore become useless.

There's one serious thing speaking against this solution, however. If there's going to be more methods like this one (e. g. make_baz(Foo f), etc.), I'd have to create an entire API of friend helper functions, which is clearly not something desirable.

My question is - is there anything better I could do to scream out loud to the users of my class that once you call it, the object becomes useless?

7
  • 8
    Perhaps a method which renders the entire object invalid is not the best design? If you feel as though you need to scream something about the side effects of a method then something feels off.
    – Dan Wilson
    Jan 17, 2020 at 14:34
  • The problem originated, because I was trying to change object's API depending on the last called method. Combined with the fact that I'm working with actual file descriptors which can't be copied, it seems like a one of two evils. The other would be giving the user access to all methods at once, allowing them to be called in nonsensical combinations.
    – mdx
    Jan 17, 2020 at 14:49
  • 4
    How about a method that requires this object to be an rvalue reference, as created by std::move? E.g.: Bar transformIntoBar() &&
    – amon
    Jan 17, 2020 at 15:04
  • 2
    @amon that should be an answer
    – RiaD
    Jan 17, 2020 at 15:29
  • 2
    @TuRtoise The outer move is unnecessary. The inner move is unavoidable since foo would be an lvalue reference. That's just C++ for you! However, temporaries would already be rvalues. E.g. getFooByValue().transformIntoBar() would work without explicit moves.
    – amon
    Jan 17, 2020 at 16:15

4 Answers 4

9

As mentioned by amon in a comment: You can require this to be an rvalue reference by adding && to the function signature:

Bar transformIntoBar() &&
{
    ...
}

This way, the following code fails to compile:

Foo foo;
Bar bar = foo.transformIntoBar();
// compile error (clang):
// 'this' argument to member function 'transformIntoBar' is an lvalue, but function has rvalue ref-qualifier

However, the following does compile:

Foo foo;
Bar bar = std::move(foo).transformIntoBar();

Per convention, many objects that were moved from are in some "special" state, so C++ developers know that they should not use foo anymore, or that they should look at the documentation to find out in which state foo is after being moved from.

This also nicely handles cases where a temporary (unnamed) Foo object is created, because such an object is already an rvalue:

Bar bar = createFoo().transformIntoBar();
1
  • After all the hassle I eventually went with this solution and (as I said in the comments to the OP) it actually does a pretty good job.
    – mdx
    Jan 24, 2020 at 12:13
1

Could do with more information on the specific case. But you might want to try a builder object and fluent pattern

Bar b = Builder.CreateFoo() //ISettableFile
    .SetFile() //IConvertable
    .ConvertToBar() // Bar

Foo f = Builder.CreateFoo() //ISettableFile
    .SetFile() //IConvertable
    .ConvertToFoo() // Foo

use interfaces and private constructors to limit access to the underlying objects or conversion functions

0

Normally, when you need to retain control of an object's lifecycle, you are better served not giving an object directly to the user code. This is especially true for I/O stuff which require open/close logic additionally.

What I mean is, instead of returning the object, require the logic using the object to be supplied to you. Of course you supply the object then to the logic, but then the object has a clear lifecycle which you manage.

At the class you've showed this is too late I think, you would need a different design one level above. Unfortunately I know too little of C++ to give you a concrete example, but I hope the concept itself helps.

1
  • Care to lend me a hand and elaborate a bit? To be fair, I feel like every syntax-based solution, even if working, will make the code more obscure, so perhaps a different concept is just what I need.
    – mdx
    Jan 17, 2020 at 16:12
0

As it looks like C++, consider a cast only applicable to rvalues:

class Foo {
    ...
    operator Bar() && {
        Bar r(m_fileDescriptor);
        m_fileDescriptor = -1;
        return r;
    }
};

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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