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?
boost::filesystem::path is the central class in Boost. Filesystem for representing and processing paths. Definitions can be found in the namespace boost::filesystem and in the header file boost/filesystem. hpp . Paths can be built by passing a string to the constructor of boost::filesystem::path (see Example 35.1).
A filesystem is a structure for the computer to store the files and folders that make up the data of the operating system. Inside a filesystem, folders are referred to as directories. Folders that exist inside other folders are called subdirectories.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With