Compressing syntax/code, a testcase

D

dizzy

Hi

I've started lately to run more often than not into situations where I need
a function to perform several duties, but the duties to perform (although
decided at the point of call, at compile time) are not controlled by the
signature (the arguments are the same) so overloading normally won't help
(unless abused like new-expression does with std::nothrow). There is also a
need to remove any redudant or duplicated code. I will take a "real world"
example, supose I want to "beautify" and extend/wrap the mkdir(2) POSIX
call. Requirements (acording to the code that will use it):

1. beautify:

1.1 it will take not only char const* for pathname but also std::string
const& (most likely to be used alot more oftenthan the char const* version)
1.2 also, mkdir(2) takes 2 arguments, a path and a second argument which
represents the privileges to be created with for that directory;
beautification asks us to make this second argument a default argument of
value 0755

2. extend/changed semantics:

2.1 there must be a nothrow version that returns with bool if it succeeded
2.2 there must be a throw version that throws in case of error
2.3 there must be a version where the code should NOT consider an error if
mkdir(2) returns -1 and errno == EEXIST and there is a directory already
2.4 there must be a version to be used when the caller does not care about
any possible failure (best effort version, this version is needed because
in order to properly solve requirement 2.3 one needs to use an expensive
(l)stat because EEXIST is also returned in case of non directories existing
at the same path and as such, some users may just want a best effort mkdir
without the cost of doing stat in case of EEXIST failure)

Sounds simple and straightforward :)

(the char const* taking versions are not showed for brevity although the
real code has the std::string const& taking versions be just inline
relayers to the char const* versions using .c_str() on the received path)

bool mkdir_nothrow(std::string const& path, mode_t privs = 0755) {
struct stat stbuf;
return !::mkdir(path.c_str(), privs) || (errno == EEXIST
&& !::lstat(path.c_str(), &stbuf) && S_ISDIR(stbuf.st_mode));
}

void mkdir(std::string const& path, mode_t privs = 0755) {
if (!mkdir_nothrow(path, privs))
throw std::runtime_error("mkdir");
}

The above code should satisfy all requirements except 2.4. To satisfy 2.4
one may argue the user may just directly call mkdir(2) which does just
that, but mkdir(2) does not satisfy the beautification requirements.
Because I want "syntax compression" (that is short, expressive syntax) for
all these mkdir cases I thought a better solution is needed, something
like:

enum {
mkdir_nothrow = 0,
mkdir_throw = 1,
mkdir_noexists = 0,
mkdir_exists = 2
}

template<unsigned Options>
typename if_<Options & mkdir_throw, void, bool>::type
mkdir(std::string const& path, mode_t privs = 0755) {
bool res = !::mkdir(path.c_str(), privs);
if (Options & mkdir_exists) {
struct stat stbuf;
res ||= errno == EEXIST && !::lstat(path.c_str(), &stbuf) &&
S_ISDIR(stbuf.st_mode);
}

if ((Options & mkdir_throw) && !res)
throw std::runtime_error("mkdir");
return typename if_<Options & mkdir_throw, void, bool>::type(res);
}

Well, maybe the implementation could be better (please provide better
examples, I'm pretty new into this kind of coding style), I realise I could
use some template specializations instead of relying on the compiler
optimizer with "if (Options & various)" being optimized out based on
compile time known values and also the last line/conversion. But I am more
interested to know if such way to solve things is fairly common, I have a
strong feeling I'm missing something big that I had to write all this stuff
just to have flexible behaviour control over a simple function like that.

The end result is pretty nice tho:
mkdir<mkdir_nothrow | mkdir_exists>(path);
or mkdir<mkdir_throw | mkdir_noexists>(path, 0700);

How do you people solve similar issues?
 
C

Christopher

How do you people solve similar issues?

Coding standards per project.
i.e
All code in this project will use exceptions for error handling.
All text parameters will be in the form of const char * ( a string can
always call c_str() )

Design decisions
i.e
Permissions are a parameter or they aren't.
If a directory exists, no operation is performed.


Trying to cover every combination is a wasted and bloated effort IMO.
You cannot cater to everyone, but you can be consistent.
Spending too much time and providing that many options for something
as simple as making a directory would get me fired.

There is a time when one can program too generically.
 
D

dizzy

Christopher said:
Coding standards per project.
i.e
All code in this project will use exceptions for error handling.
All text parameters will be in the form of const char * ( a string can
always call c_str() )

Design decisions
i.e
Permissions are a parameter or they aren't.
If a directory exists, no operation is performed.


Trying to cover every combination is a wasted and bloated effort IMO.
You cannot cater to everyone, but you can be consistent.
Spending too much time and providing that many options for something
as simple as making a directory would get me fired.

But mkdir(2) is a low level, common need that for sure will be needed for
almost any other project and I intend to reuse as much code as possible
(not have to rewrite mkdir for every different project because it has other
coding style or design decisions). For mkdir needs I would have used
boost::filesystem::create_directory if that supported privileges on
creation (to avoid an addition chmod system call).
There is a time when one can program too generically.

I might have reached such a time but the code I write I intend to reuse in
any possible project that would need something like mkdir so I need alot of
flexibility from it and (as any library code) to achieve the "do not pay
for what you don't need" design principle.
 
J

James Kanze

I've started lately to run more often than not into situations
where I need a function to perform several duties, but the
duties to perform (although decided at the point of call, at
compile time) are not controlled by the signature (the
arguments are the same) so overloading normally won't help
(unless abused like new-expression does with std::nothrow).
There is also a need to remove any redudant or duplicated
code. I will take a "real world" example, supose I want to
"beautify" and extend/wrap the mkdir(2) POSIX call.
Requirements (acording to the code that will use it):
1. beautify:
1.1 it will take not only char const* for pathname but also
std::string const& (most likely to be used alot more oftenthan
the char const* version) 1.2 also, mkdir(2) takes 2 arguments,
a path and a second argument which represents the privileges
to be created with for that directory; beautification asks us
to make this second argument a default argument of value 0755

That's just overloading and a default argument. (For that
matter, you could argue that the overloading isn't necessary,
since a char const* converts implicitly to an std::string.)

On the other hand, you don't want to tie the modes down that
much to the underlying OS. I use symbolic constants, open and
closed: the default is closed (which maps to 0700 under Posix).
open maps to 0775, but you could easily define more (probably
accepting that some will end up mapping to the same thing on
some other systems).
2. extend/changed semantics:
2.1 there must be a nothrow version that returns with bool if
it succeeded 2.2 there must be a throw version that throws in
case of error 2.3 there must be a version where the code
should NOT consider an error if mkdir(2) returns -1 and errno
== EEXIST and there is a directory already 2.4 there must be a
version to be used when the caller does not care about any
possible failure (best effort version, this version is needed
because in order to properly solve requirement 2.3 one needs
to use an expensive (l)stat because EEXIST is also returned in
case of non directories existing at the same path and as such,
some users may just want a best effort mkdir without the cost
of doing stat in case of EEXIST failure)
Sounds simple and straightforward :)

There are really only two options: you report an error if the
creation fails, or you don't, if the directory already exists.
Logically, it's two different operations: createDirectory(), and
ensurePath(). With two different names: the first creates the
named directory, and fails if it can't; the second ensures that
the path exists, and fails if the path doesn't exist (when it
has finished). (Note that ensurePath behaves like 'mkdir -p'
under Posix---it also creates any intermediate directories that
might be needed.)

You might also want to consider a more elaborate error code,
with different types of error conditions.

For something this low level, the only really appropriate way to
report an error is with a return code. If the client code wants
an exception, it can wrap the function (and if it really wants
to ignore any errors, it can do that as well).
 
D

dizzy

James said:
That's just overloading and a default argument. (For that
matter, you could argue that the overloading isn't necessary,
since a char const* converts implicitly to an std::string.)

Correct, the char const* uses are very few to worth those versions.
On the other hand, you don't want to tie the modes down that
much to the underlying OS. I use symbolic constants, open and
closed: the default is closed (which maps to 0700 under Posix).
open maps to 0775, but you could easily define more (probably
accepting that some will end up mapping to the same thing on
some other systems).

Good idea, in my case the code is to be used only on POSIX systems but I'll
keep that in mind because I too like to write portable code even when not
required.
There are really only two options: you report an error if the
creation fails, or you don't, if the directory already exists.
Logically, it's two different operations: createDirectory(), and
ensurePath().

Good catch, indeed these are 2 separate operations I now realise. Although
your choice of name is slightly bad, as you realised one thing is
ensurePath() that is more similar to mkdir -p and another would be say
an "ensureDirectory()" or something like that.
You might also want to consider a more elaborate error code,
with different types of error conditions.

Could you elaborate?

For something this low level, the only really appropriate way to
report an error is with a return code. If the client code wants
an exception, it can wrap the function (and if it really wants
to ignore any errors, it can do that as well).

It does appear tho that all projects that will use this code will prefer
usually the throw version (except some other low level code which usually
builds upon nothrow versions) which is why I thought I need both.

Thanks for your sugestions.
 
J

James Kanze

Correct, the char const* uses are very few to worth those versions.
Good idea, in my case the code is to be used only on POSIX
systems but I'll keep that in mind because I too like to write
portable code even when not required.
Good catch, indeed these are 2 separate operations I now
realise. Although your choice of name is slightly bad, as you
realised one thing is ensurePath() that is more similar to
mkdir -p and another would be say an "ensureDirectory()" or
something like that.

To tell the truth, I don't remember where I got the name from.
But the "ensure" is the significant part: if the directory
already exists, then it's not an error. Whereas it is with
createDirectory.
Could you elaborate?

Some client code might want to know why the operation failed:
insufficient rights, hardware problem, missing sub-directory (in
the case of createDirectory), etc.
It does appear tho that all projects that will use this code
will prefer usually the throw version (except some other low
level code which usually builds upon nothrow versions) which
is why I thought I need both.

The most frequent use in my code is to create a subdirectory for
temporaries. If this fails, I abort.

In general, with such functions, it's a difficult call. But in
general, any time it the issue is unsure, I use a return code;
it's very, very easy to convert a return code into an exception
(or the equivalent of an assertion failure) if that's what is
needed; converting an exception into a return code is somewhat
more difficult (and converting an abort into a return code is,
of course, completely impossible). And of course, you're not
calling this function hundreds of places in an application, so
it's not as if wrapping it were a big deal.

Another alternative that I played with in the past is a user
defined callback. The callback normally does nothing, and the
return code is returned, but it could throw an exception. In
the end, I found this too much complication, particularly for
the immediate clients, who didn't necessarily know exactly what
to expect.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,769
Messages
2,569,582
Members
45,071
Latest member
MetabolicSolutionsKeto

Latest Threads

Top