in this PIMPL implementation , we basically converting compile time into run time. why?
O_o
I'm fine with what Elysia had to say. The supposed costs though of correct, reliable, and clean code is an issue with me thanks to repeated cowboy exposure to over the years so I decided to put my own spin on what is in some ways the same arguments.
You asked about two aspects, but what about reliability?
Even in C++98 code, you want to be able to perform certain operations without risking the possibility of failure. A primitive that doesn't provide certain exception guarantees is difficult to make exception safe and impossible to make exception neutral. In order to make such a guarantee for some operations, you need to be able to attempt the operation before then committing results on success without then introducing a possible point of failure.
Code:
class SWhatever
{
// ...
void doSomething(/* ... */)
{
mData1 = GetUsefulData1(/* ... */);
mData2 = GetUsefulData2(/* ... */);
// ...
}
SData1 mData1;
SData2 mData2;
};
Let's assume the assignments can throw an exception. As is, the code appears to provide at least a weak guarantee. In reality, we don't know that `mData1` and `mData2` doesn't have some interrelationship which the `SWhatever` class is packaging. We need at least the weak guarantee for clients to be able to make any guarantees. We are assuming the assignment can throw an exception so we can't just swap back and forth using a temporary. (The default behavior of `std::swap` is using a temporary for storage.) We need a means to treat the two assignments as if they were a single atomic operation. You might try some `catch` nonsense, or you might try moving the two members into a separate class, but you'll find that neither layers of `catch` blocks or moving the members into a separate class will buy anything worth having developed. We need a way to say "copy this data without the possibility of failure", but we can never implement such a beast by somehow chaining operations which may fail. In order to buy what we want, we must somewhere use an operation which can't fail. Outside of trivial classes, the only assignment operations which can't fail are exclusive to native types. Handily, the pointers are native types.
Code:
class SWhatever
{
// ...
void doSomething(/* ... */)
{
SData1 * sData1(0);
SData2 * sData2(0);
try
{
sData1 = new SData1(GetUsefulData1(/* ... */));
sData2 = new SData2(GetUsefulData2(/* ... */));
// If we got to this point, we know the assignment and allocation was successful.
{
SData1 * sTemporary1(mData1);
SData2 * sTemporary2(mData2);
mData1 = sData1;
mData2 = sData2;
delete sTemporary1;
delete sTemporary2;
}
}
catch(...)
{
delete sData1;
delete sData2;
throw;
}
// ...
}
SData1 * mData1;
SData2 * mData2;
};
Yuck. The code isn't exception neutral, has several bits of duplication, potentially wastes time allocating an object which can't be used. The code has a lot has a lot of problems, but the `doSomething` method is reliable. Clients can use the `doSomething` method without worrying if a problem will leave them holding an object which isn't useful.
Now, let's use some standard primitives so we don't have such problems.
Code:
class SWhatever
{
// ...
void doSomething(/* ... */)
{
std::unique_ptr<SData1> mData1(new SData1(GetUsefulData1(/* ... */)));
std::unique_ptr<SData2> mData2(new SData2(GetUsefulData2(/* ... */)));
// If we got to this point, we know the assignment and allocation was successful.
mData1.swap(mData1);
mData2.swap(mData2);
}
std::unique_ptr<SData1> mData1;
std::unique_ptr<SData2> mData2;
};
Yay. We have some pretty clean code while still implementing a reliable method.
Of course, the `SWhatever` method is most likely going to need to implement more than one method operating on both members. We don't like repetition so we can package the bits into an interface making the use a little more palatable, and we even have a performance related excuse because two allocations probably always cost more than one allocation.
Code:
namespace Detail
{
class SWhateverData
{
SWhateverData
(
const SData1 & fData1
, const SData2 & fData2
):
mData1(fData1)
, mData2(fData2)
{
}
SData1 mData1;
SData2 mData2;
};
}
class SWhatever
{
// ...
void doSomething(/* ... */)
{
mData = new Detail::SWhateverData(GetUsefulData1(/* ... */), GetUsefulData2(/* ... */));
}
std::unique_ptr<Detail::SWhateverData> mData;
};
You'll find that implementing such behaviors, with some mechanism, is very common in the C++ language. The tools of idiomatic C++ demands making some guarantees to clients which sometimes costs an allocation, developing indirect layers, or even just some tedious hoop jumpery. The execution and development costs of then layering the "Cheshire Cat" idiom are trivial. We are almost at the "Cheshire Cat" level just by virtue of writing a robust method with idiomatic C++ code.
Soma