tags:

views:

126

answers:

3

Hello, everyone :)

I have a useful macro here:

#include <algorithm>
#include <vector>
#include <string>
#include <boost/algorithm/string.hpp>
#include <Windows.h>

namespace Path {

bool Exists(const std::wstring& path)
{
    DWORD result = GetFileAttributesW(path.c_str());
    return result != INVALID_FILE_ATTRIBUTES;
}

// THIS IS THE MACRO IN QUESTION!
#define PATH_PREFIX_RESOLVE(path, prefix, environment) \
if (boost::algorithm::istarts_with(path, prefix)) { \
    ExpandEnvironmentStringsW(environment, buffer, MAX_PATH); \
    path.replace(0, (sizeof(prefix)/sizeof(wchar_t)) - 1, buffer); \
    if (Exists(path)) return path; \
}

std::wstring Resolve(std::wstring path)
{
    using namespace boost::algorithm;
    wchar_t buffer[MAX_PATH];
    trim(path);
    if (path.empty() || Exists(path)) return path;

    //Start by trying to see if we have a quoted path
    if (path[0] == L'"') {
        return std::wstring(path.begin() + 1, std::find(path.begin() + 1, path.end(), L'"'));
    }

    //Check for those nasty cases where the beginning of the path has no root
    PATH_PREFIX_RESOLVE(path, L"\\", L"");
    PATH_PREFIX_RESOLVE(path, L"?\?\\", L"");
    PATH_PREFIX_RESOLVE(path, L"\\?\\", L"");
    PATH_PREFIX_RESOLVE(path, L"globalroot\\", L"");
    PATH_PREFIX_RESOLVE(path, L"system32\\", L"%systemroot%\\System32\\");
    PATH_PREFIX_RESOLVE(path, L"systemroot\\", L"%systemroot%\\");

    static std::vector<std::wstring> pathExts;
    if (pathExts.empty()) {
        #define MAX_ENVVAR 32767
        wchar_t pathext[MAX_ENVVAR];
        DWORD length = GetEnvironmentVariableW(L"PATHEXT", pathext, MAX_ENVVAR);
        if (!length) WindowsApiException::ThrowFromLastError();
        split(pathExts, pathext, std::bind2nd(std::equal_to<wchar_t>(), L';'));
        pathExts.insert(pathExts.begin(), std::wstring());
    }
    std::wstring::iterator currentSpace = path.begin();
    do {
        currentSpace = std::find(currentSpace, path.end(), L' ');
        std::wstring currentPath(path.begin(), currentSpace);
        std::wstring::size_type currentPathLength = currentPath.size();
        typedef std::vector<std::wstring>::const_iterator ExtIteratorType;
        for(ExtIteratorType it = pathExts.begin(); it != pathExts.end(); it++) {
            currentPath.replace(currentPathLength, currentPath.size() - currentPathLength, *it);
            if (Exists(currentPath)) return currentPath;
        }
        if (currentSpace != path.end())
            currentSpace++;
    } while (currentSpace != path.end());

    return path;
}

}

It's used about 6 times within the scope of a single function (that's it), but macros seem to have "bad karma" :P

Anyway, the problem here is the sizeof(prefix) part of the macro. If I just replace this with a function taking a const wchar_t[], then the sizeof() will fail to deliver expected results.

Simply adding a size member doesn't really solve the problem either. Making the user supply the size of the constant literal also results in a mess of duplicated constants at the call site.

Any ideas on this one?

+3  A: 

Pass the array by reference, using a template to infer the length. I'll go looking for an example, but basically:

template<size_t N>
bool func(const char (&a)[N], blah, blah) { ... }

EDIT: Someone explained it here: http://heifner.blogspot.com/2008/04/c-array-size-determination.html

Ben Voigt
+1  A: 

Have you tried replacing sizeof(prefix)/sizeof(wchar_t) with wcslen(prefix)?

Dewb
That incurs a runtime penalty which I'd like to avoid.
Billy ONeal
But you're not concerned with the calls to `ExpandEnvironmentStringsW()` or `Exists()`? Remember that the copiler is likely to inline `wcslen()` probably at least as readily as `wstring::replace()` or `boost::algorithm::istarts_with()`.
Michael Burr
@Michael Burr: `sizeof()` takes zero time. `wcslen()` takes a nonzero time. It has nothing to do with whether or not the function can be inlined.
Billy ONeal
My point is that correctness and maintainability are likely to be more important the the negligible performance gain you'll get by avoiding a `wcslen()` call - particularly since there's other stuff there that probably far more expensive and this is also probably not a hotspot.
Michael Burr
@Billy: It takes 110 ms to call `Path::Exists` 10 000 times for me (crudely timed with `clock`). On the other hand, 10 000 calls to `wcslen` with short string like paths are takes no measurable time. `wcslen` may be a hundred times slower than using `sizeof` for this particular operation, but in the bigger picture it can be expected to make (far) less than 1% of difference in this function. In other words, nobody is going to notice any difference because the bulk of the time in this function is being spent in a different place.
visitor
+3  A: 

Why not just make it a regular function that uses wcslen() to get the length of the parameter you're interested in. There's enough stuff going on in that macro/function that I imagine there's little value trying to force it to be inlined. The 'overhead; of the wcslen() call and processing is almost certainly not going to be a bottleneck.

The only trick in the macro that you really should be concerned with is (as GMan pointed out) the return from the within the macro that's hidden when the macro is invoked.

Just have the thing be a function that returns a success/fail, and you can return if the function succeeds:

bool PathPrefixResolve( std::wstring& path, wchar_t const* prefix, wchar_t const* environment)
{
    wchar_t buffer[MAX_PATH];

    if (boost::algorithm::istarts_with(path, prefix)) {
        ExpandEnvironmentStringsW( environment, buffer, MAX_PATH);

        std::wstring tmp( path);

        tmp.replace(0, wcslen( prefix), buffer);
        if (Exists(tmp)) {
            path = tmp;
            return true;
        }
    }

    return false;
}

to use the function:

//Check for those nasty cases where the beginning of the path has no root
if (PathPrefixResolve2(path, L"\\", L"")) return path;
if (PathPrefixResolve2(path, L"?\?\\", L"")) return path;
if (PathPrefixResolve2(path, L"\\?\\", L"")) return path;
if (PathPrefixResolve2(path, L"globalroot\\", L"")) return path;
if (PathPrefixResolve2(path, L"system32\\", L"%systemroot%\\System32\\")) return path;
if (PathPrefixResolve2(path, L"systemroot\\", L"%systemroot%\\")) return path;

Given what the processing that's occurring in the macro, I don't think you need to be worried about function call overhead.

Also, your macro implementation has some behavior which I think is probably a bug - if the path starts with L"\\?\\" that means it also starts with L"\\" and your first invocation of the macro:

PATH_PREFIX_RESOLVE(path, L"\\", L"");

will change the path variable. As the program gets maintained and additional prefixes get added, the problem could be seen with other path prefixes. This bug isn't in the function version, since the function changes the path parameter only when there's a verified match.

However, there's still possibly an issue when dealing with the L"\\?\\" and L"\\" prefixes in that both might be a match - you need to make sure you pass in the prefixes that might match more than once in 'priority' order.

Michael Burr
That is intended behavior of the macro. ` \ ` should never start a valid path, so it should be stripped from the path. I'm not saying `wcslen` will be a bottleneck here, but I'm not going to replace a perfectly working piece of code with code that incurs a runtime penalty without good reason. The fact that "macros have bad karma" is not reason enough.
Billy ONeal
But if you strip the `"\\"` then you'll never match the `"\\?\\"` so there's either a bug or you're looking for something unnecessarily. As far as the argument about changing the code - wasn't your question about the fact that using `sizeof()` in the macro was causing some sort of problem? I'm pretty sure I didn't bring it up initially.
Michael Burr
I'm looking for `"\\\\?\\"`. I have accounted for the removal of the first slash in my argument for macros further down the line. It's not so much that the macro is a problem -- the only problem is that macros seem to be frowned upon.
Billy ONeal
This macro is a great example of something that is *begging* to cause maintainability problems down the line. That is why they're frowned upon, and is a pretty solid justification for incurring a runtime penalty at this stage of the game. You've got weird scoping, invisible exit points, and you've locked yourself into using string literals for no reason other than fighting the man.
Dennis Zickefoose
@BillyONeal: If the macro currently isn't causing a problem, I think I might be inclined to agree that there's no reason to muck around with it. It's not something I'd have written, but if it's already there and there's no other reason to change it, I can understand leaving it be. However, if some reason to change the code does arise, I'd certainly make it a non-macro and remove the non-obvious side-effects and exit points. Also, I think that focusing on the performance aspects of the macro would be following a red herring.
Michael Burr
@BillyONeal: "I'm looking for `"\\\\?\\"`. I have accounted for the removal of the first slash..." - I see; that's something I didn't consider and I missed (you might want to throw in a comment about what you're doing there - I think it's non-obvious).
Michael Burr
@Michael Burr: OK. +1.
Billy ONeal