Próbuję użyć rekurencji z szablonami variadic. Chciałbym, aby przypadek podstawowy miał zero argumentów szablonu. Po przejrzeniu odpowiedzi stackoverflow na poprzednie pytania, znalazłem dwa rodzaje odpowiedzi na ten problem:

  1. Nie powinieneś specjalizować się w funkcjach szablonów. Herb Sutter napisał o tym tutaj: http://www.gotw.ca/publications/mill17.htm
  2. Używasz template <typename = void> lub template <typename T = void> . Na przykład pierwsza odpowiedź tutaj: Jak napisać funkcję rekurencyjną szablonu variadic ?

Próbowałem użyć rozwiązania (2) w moim problemie, ale otrzymałem błędy. To jest minimalny, powtarzalny przykład:

#include <iostream>

template<typename = void> // base case
int NumArguments() {
    return 0;
}

template<typename FirstArg, typename... RemainingArgs>
int NumArguments() {
    return 1 + NumArguments<RemainingArgs...>();
}
class A {
public:
    A() {}
};

int main() {
    std::cout << NumArguments<A>();
    return 0;
}

Kompilacja w Microsoft Visual C++20 dała błędy:

 error C2668: 'NumArguments': ambiguous call to overloaded function
 message : could be 'int NumArguments<A,>(void)'
message : or       'int NumArguments<A>(void)'
message : while trying to match the argument list '()'

Co oznacza ten komunikat o błędzie? Jak utworzyć przypadek bazowy z zerowymi argumentami dla rekurencji za pomocą szablonów wariadycznych?

Edytuj: w komentarzach pojawiły się prośby o pełniejszy opis mojego problemu. Pytanie tak naprawdę jest tytułem pytania, a nie „jak sprawić, by mój kod działał?”, ale nie udało mi się jeszcze skompilować kodu, więc postanowiłem się nim podzielić.

NumArguments jest substytutem innej funkcji ComputeSize, która przyjmuje jako dane wejściowe Args i zwraca std::size_t.

    template<typename = void>
    constexpr std::size_t ComputeSize() {
        return 0;
    }

    template<typename FirstArg, typename... RemainingArgs>
    constexpr std::size_t ComputeSize() {
        return FuncReturnSize<FirstArg>() + ComputeSize<RemainingArgs...>(); 
    }

Możliwa lista Arg w Args jest skończona i znana przed kompilacją. FuncReturnSize jest przeciążony dla każdego z tych Args. Na przykład dwa możliwe „przeciążenia”(?) to

template <typename T>
requires ((requires (T t) { { t.Func()} -> std::same_as<double>; }) || (requires (T t) { { t.Func() } -> std::same_as<std::vector<double>>; }))
constexpr std::size_t FuncReturnSize() {
    return 1;
}

template <typename T>
requires requires (T t) { { t.Func() } -> is_std_array_concept<>; }
constexpr std::size_t FuncReturnSize() {
    return std::tuple_size_v<decltype(std::declval<T&>().Func())>;
}

Koncepcja is_std_array_concept<> powinna sprawdzić, czy wartość zwracana przez t.Func() jest jakąś tablicą rozmiarów. Nie jestem jeszcze pewien, czy to działa. Jest zdefiniowany przez

    template<class T>
    struct is_std_array : std::false_type {};
    template<class T, std::size_t N>
    struct is_std_array<std::array<T, N>> : std::true_type {};
    template<class T>
    struct is_std_array<T const> : is_std_array<T> {};
    template<class T>
    struct is_std_array<T volatile> : is_std_array<T> {};
    template<class T>
    struct is_std_array<T volatile const> : is_std_array<T> {};

    template<typename T>
    concept is_std_array_concept = is_std_array<T>::value;

Chcę, aby wszystkie te obliczenia były wykonywane w czasie kompilacji, więc zdefiniowałem

template<std::size_t N>
std::size_t CompilerCompute() {
    return N;
}

Powinienem być teraz w stanie ComputeSize w czasie kompilacji w ten sposób:

CompilerCompute<ComputeSize<Args...>()>()
5
mana 18 czerwiec 2021, 23:20

5 odpowiedzi

Najlepsza odpowiedź

Oto inne rozwiązanie (bez specjalizacji), które wykorzystuje klauzulę C++20 requires do rozwiązania niejednoznaczności:

template <typename... Args> requires (sizeof...(Args) == 0)
constexpr int NumArguments() {
    return 0;
}

template<typename FirstArg, typename... RemainingArgs>
constexpr int NumArguments() {
    return 1 + NumArguments<RemainingArgs...>();
}

Przykład:

int main() {
    std::cout << NumArguments<int>() << std::endl;
    std::cout << NumArguments() << std::endl;
    std::cout << NumArguments<float, int, double, char>() << std::endl;
    return 0;
}
1
0
4

EDYTOWAĆ: Moja stara sugestia dotycząca korzystania z concepts była nieprawidłowa. tutaj znajdziesz dobry post na temat korzystania z pojęć i pakietów parametrów.

1
Jack Harwood 19 czerwiec 2021, 00:41

Niestety nie udało mi się uruchomić koncepcji is_std_array, ale jeśli chodzi o NumArguments<T...>(), można to zrobić za pomocą wyrażenia fold:

template<typename ...T>
int NumArguments()
{
    return (FuncReturnSize<T>() + ...);
}

Wyrażenie fold tutaj zostanie rozwinięte w następujący sposób:

return (((FuncReturnSize<T1>() + FuncReturnSize<T2>()) + FuncReturnSize<T3>()) + FuncReturnSize<T4>)

Próbny

Tutaj specjalizowałem std::integral i std::floating_point wersję FuncReturnSize(), a każdy inny typ po prostu zwróciłby sizeof(T). I powinieneś być w stanie łatwo specjalizować się w innych typach, mając zdefiniowaną dobrą koncepcję.

Uwaga, zrobiłem również FuncReturnSize()s consteval.

0
Ranoiaetep 19 czerwiec 2021, 00:09

W rozwiązaniu, które próbowałeś dostosować, szablon przypadku bazowego używa innego rodzaju parametru szablonu niż ten, który przyjmuje parametry variadic, który przyjmuje parametry szablonu inne niż typ. Pozwala to na preferowanie przypadku podstawowego, gdy lista parametrów w miejscu wywołania jest pusta.

Możesz użyć tej samej techniki, ale odwróć ją, aby Twój przypadek bazowy przyjmował parametr szablonu inny niż typ, który będzie preferowany, gdy lista parametrów jest pusta

template<int = 0> // base case
int NumArguments() 
{
    return 0;
}

Jednak, aby rozwiązać swój problem, wcale nie potrzebujesz przypadku podstawowego. W rzeczywistości jest wbudowany operator, który pozwala robić dokładnie to, czego potrzebujesz

template<typename... Args>
int NumArguments() 
{
    return sizeof...(Args);
}
0
cigien 18 czerwiec 2021, 21:18

Powinieneś zagwarantować koniec swojego wariadowego szablonu bez przeciążania funkcji.

Rozwiązanie kompilujące się ze standardem c++ 17 (w Microsoft Visual /std:c++17) jest następujące:

#include <iostream>

//Remove or comment base case!

template<typename FirstArg=void, typename... RemainingArgs>
constexpr int NumArguments() {
    if (sizeof...(RemainingArgs) == 0) 
        return 1;
    else 
        return (NumArguments<FirstArg>() + NumArguments<RemainingArgs...>());
}

class A {
public:
    A() {}
};

int main() {
    std::cout << NumArguments<A>();
    return 0;
}
0
José D. Tascón-Vidarte 18 czerwiec 2021, 21:14

Komunikat o błędzie oznacza dokładnie to, co mówi, połączenie jest niejednoznaczne.

template<typename = void> // base case
constexpr int NumArguments() {
    return 0;
}

To nie jest funkcja szablonu, która przyjmuje 0 argumentów, to jest funkcja szablonu, która przyjmuje jeden argument, który jest domyślny (więc jeśli argument nie jest określony, jest nieważny). Oznacza to, że NumArguments<A>() jest całkowicie prawidłowym wywołaniem tej funkcji.

Ale NumArguments<A>() jest również całkowicie prawidłowym wywołaniem przeciążenia wariata z pustym pakietem wariata (przeciążenie NumArguments<A,>() wymienione w komunikacie o błędzie).

To, co odróżnia sprawę od połączonego przykładu, polega na tym, że w połączonym przykładzie przeciążenie variadiac jest szablonowane w int s, a nie w typach, więc nie ma tam niejasności. Skopiowałem tę implementację tutaj:

template<class none = void>
constexpr int f()
{
    return 0;
}
template<int First, int... Rest>
constexpr int f()
{
    return First + f<Rest...>();
}
int main()
{
    f<1, 2, 3>();
    return 0;
}

Zauważ, że drugie przeciążenie f to szablon wariadyczny, w którym każdy parametr szablonu musi mieć wartość int. Wywołanie f<A>() nie dopasuje tego przeciążenia, jeśli A jest typem, więc unika się niejednoznaczności.

Nie można zadeklarować funkcji szablonu z zerowymi argumentami, więc nie masz szczęścia. Można jednak zamiast tego przekonwertować to na szablon klasy, ponieważ szablony klas mogą być częściowo wyspecjalizowane.

template <class ...Args> 
struct NumArguments;

template <>
struct NumArguments<> {
    static constexpr int value = 0;
};

template <class T, class ...Args>
struct NumArguments<T, Args...> {
    static constexpr int value = 1 + NumArguments<Args...>::value;
};

Ta konkretna implementacja mogłaby oczywiście zostać uproszczona w użyciu sizeof..., ale PO wskazał, że ich rzeczywisty przypadek użycia jest bardziej skomplikowany.

3
Kyle 18 czerwiec 2021, 21:20