7

Given below code:

#include <iostream>
#include <filesystem>

namespace fs = std::filesystem;

int main()
{
    fs::path fsBase = "/base";
    fs::path fsAppend = "/append";
    auto fsResult = fsBase / fsAppend;

    std::cout << "fsResult: " << fsResult << std::endl;
    return 0;
}

Usually, the expected result is /base/append, but it actually gives /append.

The description of fs::path::append does indicate this behavior:

If p.is_absolute() || (p.has_root_name() && p.root_name() != root_name()), then replaces the current path with p as if by operator=(p) and finishes.

However, the behavior of std::experimental::filesystem and boost::filesystem is different, that gives expected /base/append. See examples.

The question is why it behaves like this? Why does it replace the path with append() function?

Mine
  • 4,123
  • 1
  • 25
  • 46

2 Answers2

8

fsAppend is an absolute path since it starts with / and you're on a system such as POSIX where paths starting with / are absolute.

Appending one absolute path to another absolute path doesn't make any sense (to me throwing an exception would be the most natural result actually). What should the result of C:\foo.txt append C:\bar.txt be?

In experimental::fs the rule was that if the second argument's .native() started with a directory separator then it was treated as a relative path for append purposes, even though it may be an absolute path in other contexts!

The standardized filesystem clearly distinguishes absolute paths from relative paths, trying to avoid this ambiguity that arises on POSIX systems.

The write-up of the change can be found in P0492R2 US77.

Note that you can use += rather than / for concatenation (should do what you expect), or make the second argument relative before using /.

Also see this answer for further comparison between experimental and finalized.

M.M
  • 138,810
  • 21
  • 208
  • 365
  • 1
    Throwing on appending an absolute path sounds reasonable, but replacing it looks more like a bug, from my point of view. And just find out that there is no `operator +`, but `operator +=`, and it works as expected. Please edit that part and I will mark it as the accepted answer. – Mine Mar 18 '19 at 05:10
  • `operator +=` does not add `/` if `p` is not an absolute path. So `fsResult += "append"` results in `/baseappend`, not really ideal. Is there a way to stably append a path, ignoring if it's absolute path or not? – Mine Mar 18 '19 at 13:49
  • 1
    This is clearly an error on c++ side. who in their right mind would mean to replace! an absolute path by writting something like `auto fsResult = fsBase / fsAppend;`? absolutely no one intends such a thing. and going out of ones way to make this a rule is just plain wrong! what could be more reasonable is just to treat it as directory separator as it really is and not an absolute path. taking this and treating it as an absolute path is flat out unreasonable – Hossein Oct 06 '20 at 03:36
2

File names are not (morally) strings: appending a path a and a relative path b structurally answers the question

If a were the current directory, what would the path b mean?

First, if a is the current working directory, this is the relative→absolute function (although filesystem::absolute does a bit more because on Windows D:foo is neither entirely relative nor entirely absolute).

Consider, for example, the behavior of #include"…": if the file name is relative, it is first considered starting from the directory containing the file with the #include, then it is considered starting from each element of the include path (e.g., -I/start/here). Each of those can be phrased as asking the above question:

void handle_include(const std::filesystem::path &reading,
                    const std::filesystem::path &name,
                    const std::vector<std::filesystem::path> &srch) {
  std::ifstream in(reading.parent_path()/name);
  if(!in) {
    for(auto &s : srch) {
      in.open(s/name);
      if(in) break;
    }
    if(!in) throw …;
  }
  // use in
}

What should happen if name is absolute (e.g., #include"/usr/include/sys/stat.h")? The only correct answer is to use name without considering reading or s. (Here, that would inefficiently consider the same file several times, but that’s a matter of efficiency, not correctness, and affects only the error case.) Note also the related identity that a/b.lexically_proximate(a)==b; lexically_proximate can return an absolute path (when the two paths have different root names), whereas lexically_relative can fail and lose information.

This approach also avoids the gratuitously useless answer that blind concatenation gives on Windows: C:\foo\D:\bar isn’t even a valid file name, let alone one that anyone could have meant to obtain by combining its pieces. Certainly raising an exception would avoid that as well, but at the cost of preventing the above reasonable use case. There is even the case of path("c:\\foo\\bar").append("\\baz\\quux"), which keeps part of each and produces path("c:\\baz\\quux"), which is again the correct answer to the question above.

Given that no one should be writing things like

[project]
  headers=/include
  manual=/doc

there’s no reason for the right-hand operand to be absolute when this interpretation is incorrect. (Obviously, if it is, one can write base/cfg.relative_path(); this is the answer to the follow-on question in a comment.)

The inspiration for the behavior was Python’s os.path.join, which does exactly this with each argument in turn.

Davis Herring
  • 36,443
  • 4
  • 48
  • 76
  • Oof, I'm not sure if I hate it or love it. Certainly I'd never intentionally append two absolute paths to each other, but it's perfectly logical behaviour from a certain POV. – Tolar Jan 25 '23 at 08:54