C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Function overload set type information loss

From: Jonathan Wakely <cxx_at_[hidden]>
Date: Tue, 30 Jul 2024 15:46:48 +0100
On Tue, 30 Jul 2024 at 14:50, organicoman <organicoman_at_[hidden]> wrote:

> Thank you Jonathan for your discussion,
> I just want to draw your attention that what I'm talking about is not
> available within the current state of the standard.
> To understand this idea you must untie your mind from the rules set by the
> current state of C++. Be abstract.
>
> Let me explain more:
> In the current C++ standard we have 3 objects that lose their type
> information by design.
> Namely:
> 1- functions
> 2- arrays
> 3- variable template.
> At the compilation stage, we preserve the whole type signature of the
> above objects and we manipulate it, but as soon as we change the block
> scope we lose part of that information.
> e.g
> template<typename T>
> int globalVar = 123;
>
> In theory the entity: glabalVar<int>
> Is not the same as : globalVar<double>
> Otherwise why we specialize based on template type at all.!!?? We can just
> write globalVar
>
> Value wise they are both equal
> globalVar<double> == globalVar<int> // true
>
> But type wise they should not.
> Yet the current standard says that:
> std::is_same_v<decltype(globalVar<int>), decltype(globalVar<double>)> //
> true.
>
> This is incorrect in type's theory point of view.
> The rule says that, if you specialize any type based on a template, the
> two types are different, even if their values are equal.
>
> Given the above observations, and the current state of C++,
> If we try to change the standard to implement the rule above for the 3
> categories above, we will break a lot of code.
>
> So what is the solution?
> My solution is to divide the type of an object from the 3 categories
> above, into 2 parts:
> 1- an apparent type, which is implemented currently in the standard
> 2- an effective type, that reflects the real type at the time of its
> instantiation.
>
> To implement the 2nd I suggested an operator called:
> effective_decltype ();
>
> How it works:
> When you pass one object from the 3 listed above, it will return its full
> signature.
>
> e.g.
> decltype(globalVar<double>) // is int , but
>
> effective_decltype(globalVar<double>) // should return (int)<double>, to
> say that globalVar is a variable template of apparent type int
> instantiated for double
>

But what is '<int>(double)'? That's not a thing that can be represented in
C++ today. It's not a type, so you're inventing an entire new category of
"thing" in the language.

You need a *much* more complete proposal for such a thing, explaining how
it works, how to use it, and why it would be different from a meta object
proposed by the Reflection papers.



>
> Take this concept and generalize it to arrays and functions.
>
> So one can ask, how to implement this?
>
> Remember that when you call, for example a function let say:
> // declaration of function
> void useGlobalVar(int v);
> // usage of function
> useGlobalVar(globalVar<double>);
>
> The type is right there at the call site, you can capture it inside the
> function body using the suggested operator: effective_decltype()
>
> Even if you do:
> auto var = globalVar<double>;
>
> The auto key word behavior should be changed to capture the effective
> type of right hand side of the assignment. So when we call
>
> useGlobalVar(var);
>
> The effective type is preserved.
>
> One exception though is when you write:
> int var = globalVar<double>;
>
> Here you are explicitly casting the effective type of var to be a plain
> int.
>
> In conclusion:
> I know it is a liitle bit twisted abstract math, but if you squint just a
> little bit, you can catch it.
> Also the implications of this approach is very very beneficial.
>
>
> Sent from my Galaxy
>
>
> -------- Original message --------
> From: Jonathan Wakely <cxx_at_[hidden]>
> Date: 7/30/24 4:04 PM (GMT+04:00)
> To: organicoman <organicoman_at_[hidden]>
> Cc: std-proposals_at_[hidden]
> Subject: Re: [std-proposals] Function overload set type information loss
>
>
>
> On Tue, 30 Jul 2024 at 11:09, organicoman <organicoman_at_[hidden]> wrote:
>
>>
>>
>> This technique of apparent and effective type, could be generalized to C
>>> style arrays and variable templates.
>>> For example:
>>>
>>> size_t foo(const char* arr)
>>> {
>>> //inside the function arr type is just a pointer
>>> // but it's real type has an array extent N.
>>>
>>> using eff_type = effective_decltype(arr);
>>> return extract_array_size<eff_type>::value;
>>>
>>
>> How would this be possible if 'foo' is separately compiled? How can the
>> value returned by 'foo' depend on information only available at the call
>> site, not in the function body? How would you avoid ODR violations, and how
>> would you even implement this?
>>
>> The same way you implement
>> void foo(auto arg); // c++20
>>
>
> OK, that's creates a function template, equivalent to:
>
> template<typename T> void foo(T arg);
>
> So the equivalent for your example would be a function template too, which
> you can already write in C++ today (and all the way back to C++98):
>
> template<std::size_t N> size_t foo(const char (&)[N]) { return N; }
>
> And this already exists as std::size.
>
> There's no need for your effective_decltype and extract_array_size magic,
> which are not implementable. For a non-template function, what you want is
> not possible. For a template function, you can do it already.
>
> The reason you don't generally want to do that for more complicated
> functions is that you get a different foo<N> specialization for every
> different string length:
>
> foo("1");
> foo("12");
> foo("123");
>
> Each of these creates a separate specialization, so generates more code
> and increases your binary size.
>
>
>
>>
>>
>> }
>>>
>>> template<typename T, typename V >
>>> int gInteger = 1234;
>>>
>>> int main()
>>> {
>>> char arr[]="cool_idea";
>>> foo(arr); // prints 9
>>>
>>
> Why not 10? It's a const char[10] because it's a null terminated string.
>
>
>>> auto i = gInteger<double,char>;
>>> // type of i is integer
>>> std::is_same_v<
>>> effective_decltype(i),
>>> (int)<double, char>)>; //<- this is a new notation to
>>> express a variable template
>>> }
>>>
>>> The whole idea is revolving around keeping the real type of each object,
>>> even if it decays to pointer ( ex. Functions and Arrays), or it loses the
>>> full type signature ( like variable templates, and function templates).
>>>
>>
>> This sounds like magic.
>> No,
>> When you are writing a program you are explicitly passing variable names
>> of fixed types between functions and objects, but as soon as you read data
>> from IO, you will rely on a chosen type(picked by yourself) to interpret
>> that data.
>> So, when the compiler is crunching the source code, it keeps record of
>> all declared types,
>>
>
> We can do that today with templates. You're trying to make non-templates
> behave like templates, and what you want is impossible in separately
> compiled code.
>
>
>> Just the current standard chose to omit and drop the effective type of
>> arrays and functions to be of type pointer (decaying). I guess its a C
>> legacy!
>> But now we have the tools to rectify this loss of information.
>>
>
> No you don't. If your 'foo' function is defined in a separate source file
> there is no way for it to get the information you want. It's impossible.
>
>
>
>>
>>
>>> After all, a C++ source code, is just a series of inspections and
>>> manipulation of types. So the more we keep information about types the more
>>> it is useful.
>>>
>>
>> What you probably want is reflection, which would not allow you to write
>> your size_t foo(const char*) function, but if you write different
>> functions, reflection would allow you to inspect and manipulate more
>> information about your program.
>>
>> The proposed idea will help in implementing reflection.
>>
>
> It's not needed.
>
>
>> Take this example:
>>
>> template<typename T>
>> size_t
>> type_hash_value = typeid(T).hash_code();
>>
>> void bar()
>> {
>> /* we will use the apparent type of a variable to instantiate a
>> vector. And its effect type to record the type*/
>> /* remember the new notation of a variable template mentioned in
>> example above: (type_id)<Ts...> */
>>
>> std::vector< (size_t)<T> > vec;
>> vec.push_back(type_hash_value<int>);
>> vec.push_back(type_hash_value<double>);
>> vec.push_back(type_hash_value<someT>);
>>
>> size_t val;
>> std::cin >> val;
>> // IO input
>> vec.push_back(val);
>>
>> /*apparent type of val is the same as its effective type => val is not a
>> variable template yet it is still a valid entry to the vector*/
>>
>> for(const auto& elem: vec)
>> if ( std::is_same_v<effective_decltype(elem),
>> (size_t)<double>>
>> )
>> return elem;
>> }
>>
>> The elements of the vector above have two types of information: compile
>> time and runtime.
>> The size and alignment of the elements is the same as std::size_t, that's
>> for runtime.
>> The effective type of the elements is available at compile time only.
>>
>> With this facility you can implement a program wide type_id hash table,
>> and you can go back and forth between type<->value.
>>
>> It is worth mentioning that, if the effective type and the apparent type
>> are the same, this means that the inspected object is not templated, nor
>> decayed.
>>
>> I'm not 100% sure how this would be implemented, i need more research,
>> but i guess compiler wizards out there have the necessary knowledge.
>> For now it is just a floating idea.
>>
>>
>> See "Reflection for C++26" https://wg21.link/p2996 for the active work
>> in this space.
>>
>> Yes, I'm already following its progress closely.
>>
>>
>>>
>>>
>>> Hello Gents,
>>> Let:
>>> O(f) be the Set of 'function overloads' of the function named "f".
>>>
>>> Let:
>>> "Ret" be the return type of function "f".
>>>
>>> The elements of O(f) can have 2 forms:
>>>
>>> 1-> Ret f ( Vargs )
>>>
>>> 2-> Ret f <Ts...> ( Fargs )
>>>
>>> Where:
>>> 1-> "Vargs" is a variadic list of arguments types, participating or not
>>> in template type arguments deduction if "f" is a template. e.g:
>>> template<typename T, typename V>
>>> void f(T, V, double, int)
>>> Vargs = {T, V, double, int}
>>>
>>> 2-> "Fargs" is a fixed list of arguments types, not related to any
>>> template type parameter. e.g:
>>> template <typename T, typename V>
>>> void f(double, int)
>>> Fargs= {double, int} // T,V are not in the list.
>>>
>>> "Ts" is just a variadic list of template parameters types.
>>>
>>> The type of any function of the 1st form is:
>>> decltype (f) = Ret(*)(Vargs),
>>> which keeps information about the function's type template parameters
>>> participating in the function argument's list.
>>>
>>> But the type of the 2nd form is:
>>> decltype (f) = Ret(*)(Fargs);
>>> No mater what the template parameters are, the type of the 2nd form
>>> always decays to:
>>> Ret(*)(Fargs)
>>>
>>> And always lose any type information about the function's type template
>>> parameters
>>>
>>> Yet, when we want to get the address of such function, we are obligated
>>> to use the template types in the function name. e.g:
>>> auto select_f = &f<Ts...>;
>>>
>>> Otherwise we get overload ambiguity,
>>> This is a proof that the template arguments participates in the function
>>> type.
>>>
>>> In my opinion, the compiler should keep the template arguments type
>>> information.
>>>
>>> I know that changing the type of "select_f" in the example above will
>>> break a lot of code.
>>> But i have a suggestion.
>>> If the compiler can keep record of :
>>> * an apparent function type ; (the usual one)
>>> decltype (&f<Ts...>) = Ret(*)(Fargs)
>>> * and an effective function type which is:
>>> decltype (&f<Ts...>) = Ret(*)<Ts...>(Fargs)
>>>
>>> This would fix the problem without breaking any neck.
>>>
>>> Why is this useful?
>>> Take this example:
>>>
>>> struct Erased{
>>> std::any (*m_fun) (void);
>>>
>>> template<auto Func>
>>> constexpr Erased()
>>> : m_fun(Func)
>>> { }
>>>
>>> auto operator ()()
>>> {
>>> using f_type = effective_decltype (m_fun);
>>> // imagin we have a type traits that
>>> // extracts template types.
>>> using T = extract_1st_template_type<f_type>;
>>>
>>> return std::any_cast<T>( m_fun(void) );
>>> }
>>>
>>> };
>>>
>>> template <typename Ret>
>>> std::any foo()
>>> { return Ret{}; }
>>>
>>> int main()
>>> {
>>> std::vector<Erased> vec;
>>> vec.push_back(Erased<&foo<int>>{});
>>> vec.push_back(Erased<&foo<double>>{});
>>> vec.push_back(Erased<&foo<some_type>>{});
>>> for(const auto& elem: vec)
>>> DoSomethingBasedOnReturnType(elem());
>>> }
>>>
>>> Using this technique we can store the template type, then recall it back.
>>> I guess it will make type erasure more effecient.
>>>
>>> Any thoughts?
>>>
>>> Regards
>>> Nadir
>>>
>>>
>>> Sent from my Galaxy
>>>
>>> --
>>> Std-Proposals mailing list
>>> Std-Proposals_at_[hidden]
>>> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
>>>
>>

Received on 2024-07-30 14:48:08