3

I have a set functions with different arguments, but all of them have last element the same. So I need to access that last argument in my function. Looking around I found quite some solutions with template specializations (here and here), but this solution came to my mind:

template<typename ...Args>
void function( Args&&... args )
{
     auto &last = std::get<sizeof...( Args ) - 1 >( std::tie( args... ) );
}

It may look obvious, but it was not so for me in the begining. It seems to be simple and short, but does this method have any hidden overhead versus classic solutions with template specialization and helper functions/classes?

Slava
  • 43,454
  • 1
  • 47
  • 90
  • `std::get` or `std::tuple_element` does equivalent things with template specialization. The potential downside is that you instantiate `std::tuple` and `std::get` instead of potentially less template instantiation of provided solution. – Jarod42 Aug 24 '17 at 17:51
  • It depends on how the compile implements `sizeof` and how `std::tuple` (`std::tie`) is implemented. I guess this is more efficient simply because it instantiates less auxiliary types. However, there maybe very inefficient implementations of `std::tuple` that at the end is the same as other solutions (e.g. `std::tuple` is implemented recursively). In conclusion, I think is efficient, but the hidden cost is in `std::tuple`. – alfC Aug 24 '17 at 17:51
  • @FrançoisAndrieux I think you can change your comment to an answer, I would definitely +1 that. – Slava Aug 24 '17 at 18:04
  • @alfC `sizeof` should provide compile time constant, there should be no overhead, am I wrong? I would worry more about `std::tuple` and that is the question could be there runtime overhead for that. – Slava Aug 24 '17 at 18:06
  • @Slava, yes, that is what I mean. IF there is a hidden (compile time) cost, it is in the `std::tuple`. So the answer is really sensitive to how `std::tuple` is implemented. If it uses recursion it may be similar to other solutions. Your implementation looks easy because you are just exploiting the fact that someone else implemented `std::tuple`. – alfC Aug 24 '17 at 23:54
  • In C++17, you could just do `auto &last = (args, ...);`. See https://en.cppreference.com/w/cpp/language/fold – Vladimir Reshetnikov Aug 12 '18 at 01:32

2 Answers2

2

It depends on your implementation and compiler flags. Try it at godbolt.org. gcc 7.2 with -O2 seems to be able to fully optimize it when I tried with this :

#include <tuple>

template<typename ...Args>
auto function(Args&&... args)
{
    return std::get<sizeof...(Args)-1 >(std::tie(args...));
}

int main()
{
    volatile auto x = function(1, 1., 1.f);
}

It produced this :

main:
  movss xmm0, DWORD PTR .LC0[rip]
  xor eax, eax
  movss DWORD PTR [rsp-4], xmm0
  ret
.LC0:
  .long 1065353216

example

François Andrieux
  • 28,148
  • 6
  • 56
  • 87
2

It seems to be simple and short, but does this method have any hidden overhead versus classic solutions with template specialization and helper functions/classes?

It has a drawback that could be annoying in some cases. If your type has reference qualifiers on member methods, you can encounter problems by getting an lvalue reference out of it.

Let's consider the following example:

#include<utility>
#include<tuple>

struct S {
    void foo() && {}
};

template<typename T>
void f(T &&t) {
    std::forward<T>(t).foo();
}

int main() {
    f(S{});
}

Everything works fine for we have originally an rvalue reference and by forwarding the forwarding reference we can safely call the foo member method.

Let's consider now your snippet (note, the following code doesn't compile - continue reading):

#include<utility>
#include<tuple>

struct S {
    void foo() && {}
};

template<typename... T>
void g(T&&... t) {
    auto &last = std::get<sizeof...(T) - 1 >(std::tie(t...));
    last.foo();
}

int main() {
    g(S{});
}

This won't compile for foo cannot be invoked anymore on a variable having type S & because of the reference qualifier.

On the other side, by forwarding and extracting the last parameter somehow you can keep intact its type and you don't have such a problem.
As an example:

template<typename... T>
void g(T&&... t) {
    std::get<sizeof...(T) - 1>(std::forward_as_tuple(std::forward<T>(t)...)).foo();
}
skypjack
  • 49,335
  • 19
  • 95
  • 187
  • Well, you posted the disease, may as well give the man the cure. Change it to `auto&& last` (more correct for generic code) and do `std::forward>>(last).foo();` – Nir Friedman Aug 24 '17 at 19:54
  • 1
    @NirFriedman Yeah. I was working on it by I got a smoke meanwhile. I'll copy-and-paste your comment in the answer as soon as I'm back if it's fine for you. – skypjack Aug 24 '17 at 19:57
  • SImple solution would be `std::move( last ).foo();` but if there is another solution for getting last element that does not involve writing long code that could be even better. – Slava Aug 24 '17 at 20:10
  • @Slava You don't know if the last parameter was originally an lvalue or an rvalue reference, so you could incur in the same problem by using `move` if `foo` had been declared as `void foo() &` and the last parameter was an lvalue reference. – skypjack Aug 24 '17 at 20:15
  • I see, this not a problem for my particular case, as I do know type of the last element, but for generic code this could be an issue. I see latest code added, so this should be used instead? – Slava Aug 24 '17 at 20:30
  • @Slava This _can_ be used instead. Probably gurus will come showing up with a better solution. Sometimes it happens. :-) – skypjack Aug 24 '17 at 20:31