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?
4 Answers 4
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();
-
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– mdx2020年01月24日 12:13:26 +00:00Commented Jan 24, 2020 at 12:13
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
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.
-
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– mdx2020年01月17日 16:12:39 +00:00Commented Jan 17, 2020 at 16:12
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;
}
};
this
object to be an rvalue reference, as created bystd::move
? E.g.:Bar transformIntoBar() &&
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.