12

I'm working on code that involves loading a file from a path which is constructed as a concatenation of a given "base" path and a secondary relative path, loaded from another file. For example (and where I'm running into an issue), the base path is "assets/models/", and the secondary path is "maps\map.png". Straight concatenation of these two strings gives "assets/models/maps\map.png". When running on POSIX systems this fails to load. Up to now I've been sorting this out by just replacing the backslashes with forward slashes with

std::replace( path.begin(), path.end(), '\\', '/' );

but I'd like to use C++17's std::filesystem::path to do this instead.

The description of std::filesystem::path::make_preferred() suggests that it should replace the separators:

"Converts all directory separators in the generic-format view of the path to the preferred directory separator. For example, on Windows, where \ is the preferred separator, the path foo/bar will be converted to foo\bar"

When implemented in the code, however, it doesn't convert anything. I've also verified that std::filesystem::path::preferred_separator is as expected - '/'.

Am I misunderstanding the purpose of make_preferred()? Or am I just using it wrong?

Here's a cut down version of the code that doesn't work (this isn't the implemented code, but is close enough to it):

const char * loadedPath = "maps\\map.png"
std::string loadedPathStr = std::string( loadedPath );
auto wPath = std::filesystem::path( loadedPathStr );
wPath = wPath.make_preferred();

basePath = std::filesystem::path( "./a/b/" );
auto totalPath = basePath / wPath;
auto wStr = totalPath.generic_string();
std::cout << wStr << std::endl;

This outputs "./a/b/maps\\map.png"

When debugging the implemented code, it looks like wPath is optimised out; there's no way of inspecting it.

Strangely, when I compile and run this standalone test program, it works as expected:

int main(){
   assert( std::filesystem::path::preferred_separator == '/' );            
   const char * cPath = "maps\\map.png";
   std::string path = std::string( cPath );
   auto wPath = std::filesystem::path( path );
   wPath = wPath.make_preferred();
   std::string wStr = wPath.generic_string();
   std::cout << wStr << std::endl;
}

This outputs "maps/map.png". I can't read. This also outputs the incorrect value.

Anyone know whats going on here?

EDIT: Tried compiling with clang (using gcc before), and it works as expected (separator is converted). Ignore this, made a mistake in recompiling.

I'm running this on Linux, and the path exists.

5
  • Are you on a POSIX platform, or Windows? If POSIX, do you have a file named maps\map.png in the assets subdirectory models? Commented Feb 4, 2019 at 15:25
  • What is your toolchain? Commented Feb 4, 2019 at 15:25
  • Please present non-working code in MCVE form, like this. Commented Feb 4, 2019 at 15:27
  • 1
    The problem may be: on Windows \ and / are both path separators, and the \ is the preferred one. On POSIX, / is a path separator, while \ is not a path separator, so make_preferred won't convert \ (which is just a valid, normal filename character). Commented Feb 4, 2019 at 15:32
  • BTW, make_preferred and generic_string both basically do the same thing, so you shouldn't be using both of them. make_preferred is for when you want the path's internally stored string to use the generic separator. generic_string is for when you want a copy of the path string in the generic format. Commented Feb 4, 2019 at 15:37

2 Answers 2

10

Am I misunderstanding the purpose of make_preferred()?

Not entirely, but subtly yes. A directory separator (in the generic format) is either the preferred separator, or the fallback separator: /. On systems where the preferred separator is / (e.g. POSIX), directory separator is only /. On such systems make_preferred doesn't modify the path, which explains why it would be optimised out completely.

The simplest way to replace back slashes with forward slashes is std::replace. However, do note that back slashes are valid characters in file names in POSIX, so such conversion may break the use of file names that use it.

Sign up to request clarification or add additional context in comments.

3 Comments

That makes sense, thanks for explaining it. It's unfortunate that the path system won't do the conversion but practically it makes sense that it won't since the backslash is a valid filename character.
Just as a further question here, why does the standalone version work?
Seems I can't read my own terminal. Rechecked there and you're correct, it doesn't work. Sorry about that!
3

If you want to write cross-platform code with filesystem, you should try to stick with the generic format. The behavior of all other formats of filesystem strings is implementation-dependent.

An implementation that allows alternative directory separators will treat them as directory separators. But other, perfectly valid, implementations that don't recognize those separators will not recognize them. "/" is always a directory separator; whether "\" is a separator or not depends on the implementation.

make_preferred converts from the implementation's path format into the generic format. As such, it's behavior is implementation-dependent.

The main reason behind dealing in non-generic formats is when you're getting path strings from the native OS API. Such path strings will likely be in the implementation's format, so path needs to be able to recognize them and work with them. For string literals built into your program, you should always prefer the generic format (unless your application is OS-specific, or you are selecting different strings based on the OS your code is being used on).

1 Comment

Ideally I would stick with the generic format for all the string literals, but unfortunately I'm dealing with loading 3D model files which have baked filepaths where I have little-to-no control over the format. Thanks for the response though, between this and the others I see my misunderstanding of the make_preferred() function (and the overall treatment of separators)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.