Archived

This forum has been archived. Please start a new discussion on GitHub.

Why allow undefined behavior ?

While I find the Ice project interesting, I have some comments.

Having just read chapter 2 and 3 of “Distributed Programming with Ice”, I wonder why a library starting without any backward compatibility problems allow users to implement “undefined behavior” ?

In the main standard Ice main-function:

int
main(int argc, char* argv[])
{
int status = 0;
Ice::CommunicatorPtr ic;
try {
ic = Ice::initialize(argc, argv);
// …
} catch (…) {
// …
}
if (ic)
ic->destroy();
}

it is undefined behavior to forget to call “destroy”(according to page 38, footnote 2)

Why isn’t Ice::Communicator simply an object, which in it’s constructor does what Ice::initialize does and in it’s destructor does what destroy. If the constructor fails, it throws an exception

int
main(int argc, char* argv[])
{
try {
Ice::Communicator ic;
// …
} catch (…) {
// …
return 1; // or EXIT_FAILURE
}
}

It’s simpler for the user, and there is no way to implement “undefined behavior”

I know that there is the class Ice::Application, but that’s not without problems either:
* It handles exceptions, and print to stderr.
What if my application has a GUI and it should show a message box in case of errors ? In general I think that type of classes should be exception neutral
* Although there is only one virtual function to override,
* (Minor issue) It uses run-time polymorphy, while static polymorphy like “curriosly recurring template pattern” or passing in a function object would do fine.

Something like the following:


namespace Ice
{

class Communicator
{
public:
Communicator()
{ /* what ever */}
~Communicator()
{ /* what ever */}
};

template <typename T>
class Application
{
public:
int main(int argc, char* argv[])
{
try {
Communicator ic;
// ...
return static_cast<T*>(this)->run(argc, argv);
}
catch(...) {
}
}
};

} // namespace Ice

class MyApplication : public Ice::Application<MyApplication>
{
public:
int run(int , char* [])
{
//...
return 0;
}
};


int main(int argc, char* argv[])
{
return MyApplication().main(argc, argv);
}




or



namespace Ice
{

class Communicator
{
public:
Communicator()
{ /* what ever */}
~Communicator()
{ /* what ever */}
};

template <int (*run_T)(int , char* [])>
class Application
{
public:
int main(int argc, char* argv[])
{
try {
Communicator ic;
// ...
return run_T(argc, argv);
}
catch(...) {
}
}
};

} // namespace Ice

int my_run(int, char* [])
{
// ...
return 0;
}

int main(int argc, char* argv[])
{
return Ice::Application<my_run>().main(argc, argv);
}




Kind regards

Mogens Hansen

Comments

  • Unfortunately we cannot rely on the destructor. Obviously, this doesn't work for Java, because there is no guarantee that Java will invoke finalizers. But it also doesn't work for C++.

    The communicator is, like all other objects in Ice, reference counted. So let's assume that you have a CommunicatorPtr declared outside of main() (or any other object that holds a CommunicatorPtr). For example, there could be a CommunicatorPtr as a global variable. Then the communicator destructor will not be called before main() exits.

    However, it's very impotant that the communicator is destroyed before main() exists. The behavior of multiple threads in the cleanup routines after main() returns is undefined, so if several threads are still running before main() returns, all kind of things could go wrong.

    I agree with you that it would be highly desireable to remove this undefined behavior, but I don't believe there is a solution to this.
  • Re: Why allow undefined behavior ?

    Just to add to Marc's reply...
    Originally posted by Mogens Hansen
    I know that there is the class Ice::Application, but that’s not without problems either:
    * It handles exceptions, and print to stderr.
    What if my application has a GUI and it should show a message box in case of errors ? In general I think that type of classes should be exception neutral

    That's not a problem. Ice::Application does this only if you allow an exception to escape from the run() method. You can easily take full control of all exceptions by simply catching them inside your run() implementation:

    virtual void
    MyApplication run(int argc, char * [] argv)
    {
    try {
    // ...
    } catch (...) {
    // Deal with GUI message box here...
    }
    }
    * Although there is only one virtual function to override,
    * (Minor issue) It uses run-time polymorphy, while static polymorphy like “curriosly recurring template pattern” or passing in a function object would do fine.

    Yes, I guess you are right -- this would have capture the intent a little better. In practice, I don't think it matter because the run() method is called only once, so the additional overhead of the virtual function call doesn't matter.

    The only excuse I can think of is that, doing it the way we did, the C++ and Java versions are very similar whereas, using the recurring template pattern or a function object, the C++ and Java version would be very different. (In general, we have tried to keep the C++ and Java versions as close to each other as possible. This makes it easier for people who use both languages.)

    Cheers,

    Michi.