Home > Mobile >  C 20 concepts: accumulate values of all good types passed to function variadic template
C 20 concepts: accumulate values of all good types passed to function variadic template

Time:10-07

I write a function variadic template that can be used like this:

auto result{ Accum<int>(10, 20, 1.1, "string") }; // result is 31.1;

Using binary operator Accum accumulates values of types from parameter pack for which init_type{} value; arithmetical operation makes sense. If used with empty parameters pack, Accum<int>(), it just returns default constructed value of type Init:

auto result{ Accum<int>(10, 20, 1.1, "string")   Accum<int>() }; // still 31.1;

Please look at code below. As first template parameter Accum accepts type which default constructed value is used for initialization in folding expression, optional parameters pack contains values of heterogenous types which are candidates for binary addition operation. Values of types that cannot be used for binary addition operation are substituted with Init{}, and values of good types used as-is: to achieve this helper AccumValue, overloaded function template, utilized. Overload of AccumValue constrained with concept AddableC returns passed value as-is, non-constrained overload returns default constructed value of type Init.

There's also another, less general, approach: helpers are overloaded for good types, no concept is used.

Please help me to understand why 1st approach doesn't work. Is there a way to make it work? What am I missing conceptually or/and in syntax? How would you approach this task?

import <iostream>;

// Approach 1. Concept.
template <typename Init, typename T> 
concept AddableC = requires (T&& t) { Init{}   t; };

template <typename Init, typename T>
Init AccumValue(T const&)
{
    return Init{};
}

template <typename Init, typename T> requires AddableC<Init, T>
auto AccumValue(T const& value)
{
    return value;
}

template<typename Init, typename ... Args>
auto Accum(Args&& ... args)
{
    return (Init{}   ...   AccumValue<Init>( std::forward<Args>( args ) ));
}

// Approach 2. Overloads for target types.
template<typename Init, typename T> 
auto Value(T const&)
{
    return Init{};
}

template<typename Init>
auto Value(int const& i)
{
    return i;
}

template<typename Init>
auto Value(double const& d)
{
    return d;
}

template<typename Init, typename ... Args>
auto Sum(Args&& ... args)
{
    return (Init{}   ...   Value<Init >(std::forward<Args>(args)));
}

int main()
{
    auto result1{ Accum< int >(10, 20, 1.1) }; // works
    std::cout << result1 << std::endl;

    //auto result2{ Accum< int >(20, 40, 2.2, "string")}; // doesn't compile
    //std::cout << result2 << std::endl;

    auto result3{ Sum< double >(1, 2, 3.3, "asda")   Sum< double >()}; // works
    std::cout << result3 << std::endl;
}

Tested on:

Microsoft Visual Studio Community 2022 Version 17.3.5

VisualStudio.17.Release/17.3.5 32922.545 Microsoft .NET Framework

Version 4.8.04084

Installed Version: Community

Visual C 2022 00482-90000-00000-AA918 Microsoft Visual C 2022

UPD. Changed concept as following and it works as I feel it should:

template <typename Init, typename T > 
concept AddableC = requires (T && t) 
{ 
    Init{}   t; 
    t   Init{}; 
    static_cast<T>(Init{}); // add reverse cast if expect types to be convertible to Init type
};

CodePudding user response:

A string literal is of type const char[/*...*/] which can be added to an integral type. The array decays to a const char* pointer and int const char* is then pointer arithmetic advancing the pointer by the given integer value. So the string literal will never be replaced in your scheme.

You are only checking that each args individually can be applied with to Init, but you never check whether the result of that operation is of type Init as well. The type could be completely different.

In this specific case the usual arithmetic conversions imply that int double = double, but then adding an array of const char (the string literal) to that is impossible. You verified int const char[/*...*/], but not double const char[/*...*/].

Possible solutions include:

  1. Require that doesn't change the type of the accumulator, i.e. { Init{} t } -> std::same_as<Init> instead of Init{} t;. However this will cause the floating point value to be replaced as well.

  2. Force conversion of the operands to the target type instead of adding them directly, e.g.:

     template <typename Init, typename T>
     Init AccumValue(T&&)
     {
         return Init{};
     }
    
     template <typename Init, typename T>
     requires std::convertible_to<T&&, Init>
     Init AccumValue(T&& value)
     {
         return std::forward<T>(value);
     }
    

    However this will e.g. round the floating point values.

  3. Constraint your function so that only arithmetic types are non-substituted (e.g. with std::is_arithmetic). I am not sure whether that matches your intentions.

There are other possibilities, but you haven't given a use case for the function, so I don't want to list everything I can think of.

  • Related