2

The following C++ function is extracted from lines 151 - 157 here:

template <typename... T>
std::string JoinPaths(T const&... paths) {
  boost::filesystem::path result;
  int unpack[]{0, (result = result / boost::filesystem::path(paths), 0)...};
  static_cast<void>(unpack);
  return result.string();
}

The function JoinPaths("foo", "bar", "doo.txt") will return a std::string with value "foo/bar/doo.txt" (I think) so I understand the semantics of the function.

I am trying to understand the two lines before the return statement. unpack in an array of ints, but what (and why) is happening with the leading 0's and the ellipses at the end. Can someone explain how this gets expanded? Why the 0's? I assume the static_cast is there to keep the compiler from optimizing away the array?

wcochran
  • 10,089
  • 6
  • 61
  • 69
  • 1
    What part of the expansion do you not understand? Perhaps [this question](https://stackoverflow.com/questions/25680461/variadic-template-pack-expansion) explains it? – 1201ProgramAlarm Oct 20 '21 at 22:51
  • Yes ... thanks ... the answer explaining the dummy array makes sense.... so the `static_cast` is to avoid optimization that would thow the array out? – wcochran Oct 20 '21 at 22:55
  • 1
    @wcochran, no, just to suppress the warning about unused variable (array in that case). – alagner Oct 20 '21 at 22:56
  • @TedLyngmo is there a way to see the compiler's expansion? What is weird is that `unpack` is an array of `int`s ... it must be the clever use of the comma operator with the second 0... – wcochran Oct 20 '21 at 22:59
  • It could be array of anything really, int is just a generic dummy type. Comma just returns whatever's to its right hand side, dummy 0 in this case, this way one can have functions returning void being called on array initialization. Again, see @1201ProgramAlarm link. – alagner Oct 20 '21 at 23:04
  • 1
    BTW, you may also want to read [this](https://stackoverflow.com/a/51006031/4885321). tldr: the code you've posted is a pre-C++17 workaround for the lack of fold expressions in the language. – alagner Oct 20 '21 at 23:07
  • 1
    @wcochran I misunderstood the question at first and removed the comment. Yes, the second `0` is just to discard `something` in `(something, 0)` and let the result be an `int`. The first `0` is to not try to create a zero-sized array in case one calls the function without arguments – Ted Lyngmo Oct 20 '21 at 23:08
  • 1
    post C++17 you could just fold a comma operator expression right? i.e. `(result = result / boost::filesystem::path(paths), ... )`? – jwezorek Oct 20 '21 at 23:09

2 Answers2

4

As already noted while I was typing this up, it is a pre-C++17 method to perform a fold expression.

It uses a local temporary array of int, along with the comma operator, to sneak a bunch of what would otherwise be:

result = result / paths[n]

into an integer expression, ultimately producing:

int unpack[]{
  0, 
  (result = result / paths[0], 0),
  (result = result / paths[1], 0),
  ...,
  (result = result / paths[N], 0)
};

These days, the following will do:

template <typename...Pathnames>
std::string JoinPaths( Pathnames...pathnames )
{
  return (std::filesystem::path( pathnames ) / ...).string();
}
Dúthomhas
  • 8,200
  • 2
  • 17
  • 39
  • Thanks ... It would be worth mentioning the two uses of 0's -- one to avoid an empty array and one for the comma operator so that a result of type int is produced... – wcochran Oct 20 '21 at 23:26
3
int unpack[]{0, (result = result / boost::filesystem::path(paths), 0)...};

The first 0 is there to not try to create an empty array if someone calls the function with zero arguments.

(result = result / boost::filesystem::path(paths), 0)

This evaluates result = result / boost::filesystem::path(paths) and discards it. The result is the 0 to the right side of the comma.

... is a pack expansion, putting a , between each, making it into:

int unpack[]{0, (result = result / boost::filesystem::path("foo"),0), 
                (result = result / boost::filesystem::path("bar"),0),
                (result = result / boost::filesystem::path("doo.txt"),0)
            };

So, there will be four 0:s in unpack. The cast afterwards is just to disable the warning about the unused variable unpack.

Since C++17:

[[maybe_unused]] int unpack ... could be used instead to remove the warning. One could also use a fold expression and constexpr if to skip the unpack variable completely - and one could use std::filesystem instead of the boost::filesystem:

template<class... Ps>
std::string JoinPaths2(Ps&&... paths) {
    std::filesystem::path result;
    if constexpr (sizeof...(Ps)) // can't unfold empty expansion over /
        result = (std::filesystem::path(std::forward<Ps>(paths)) / ...);
    return result.string();
}
Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108