C++ Logo

std-proposals

Advanced search

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

From: organicoman <organicoman_at_[hidden]>
Date: Thu, 01 Aug 2024 02:13:37 +0400
Very well put. I agree with Tiago here.The feedback given here is being waived-off as "that's not how type theory works" or "it doesn't work on the current version of the standard but will with my proposal".Try to absorb and really think about the feedback. Understanding of how compilers work, what it can do and what it can't is important even if your proposal is about theory. It needs to be implementable.Things like "and oh my the way, the vector is constexpr even though we didn't declare it as such, because all elements can be deduced in compile time" make it look like you are creating things on the fly.How much time did you spend thinking, tinkering and exercising this idea before posting here?You need to try to break your idea first and see if it stands long before it becomes a proposal.Tiago feed back is accepted with joy, there is no doubt, but i would like to attrack the attention that, sitting inside a system too long makes you institutionalized. (Quoted from the Shawshank redemption).Em qua., 31 de jul. de 2024 18:38, Tiago Freire via Std-Proposals <std-proposals_at_[hidden]> escreveu:




Look I'm not calling out of in order to try and gate keep you. Or to put you down.
But I can tell, it's not a lot.
There's no shame of being new to the language, we have all been there.
I was a noob once to, and let me tell I had some weird misconceptions about the language to.


But unfortunately, this is a standards definition, everybody who has ever written C++ as to deal with stuff that's in the standards. And this includes very smart people, with a life time and legacy of experience. It includes
 many such people.
And I have seen many such smart people fail to put a dent into a new proposal.


It is building upon the work of many people that came before. If stuff was this easy, odds are there would been many opportunities for people in the past to have picked it up and do it already. The consequence of this is that as time progresses,
 whatever is left that can be done gets increasingly harder.


It's far more likely that you have done a mistake, than you have stumbled into something new.


I'm not saying that it is impossible for someone with little experience to be able to put on an effective
proposal that changes something in the standard. But do too the great filter, unless you are working at the very top of the edge you are not likely to succeed.


Unfortunately, you have to be this tall to take the ride.
Technically speaking, you don't have to, but it is highly recommended.


Shit is hard! Even for an expert.






One thing that I can tell you immediately from experience is that what you are asking for is impossible to implement. It cannot be done with the C++ model, and it should never be.
This is immediately apartment by things like, not knowing what the compiler can know, naive expectations of having compile time information being deduced from runtime data, not understanding how code gets compiled, and not understanding
 that template parameters (even though can be described with types) they are not a description of the type being declared.


You cannot see this because you don't have the experience necessary to understand these things and why they are important.




And I have a genuine advice.


Spend more time learning the language, try to understand why things are the way they are first. Try to understand what the compiler can know and how it does what it does. It isn't a magical application made by the Gods, people like you and me
 have written them, and they had to figure out a mechanism to get it to work.
If people with experience tell you that it doesn't work this way, you should consider their advice.
And if you don't know the technical details on how to get this to work in practice, i.e. what is the compiler should actually do to achieve this result. You should not count on "the smart programming people to do it for you" because they are
 telling you "it can't be done".
And unless you have something concrete in terms "how exactly it can be done", this conversation is pointless.










From: organicoman <organicoman_at_[hidden]>
Sent: Wednesday, July 31, 2024 10:53:26 PM
To: Tiago Freire via Std-Proposals <std-proposals_at_[hidden]>
Cc: Tiago Freire <tmiguelf_at_hotmail.com>
Subject: Re: [std-proposals] Function overload set type information loss











Sent from my Galaxy






-------- Original message --------
From: Tiago Freire via Std-Proposals <std-proposals_at_[hidden]>
Date: 8/1/24 12:44 AM (GMT+04:00)
To: std-proposals_at_lists.isocpp.org
Cc: Tiago Freire <tmiguelf_at_[hidden]>
Subject: Re: [std-proposals] Function overload set type information loss



Quick question, how many years of experience do you have writing code in C++ at a professional level?


Enough to know what I am doing.


Now to the meat of the discussion.
Do you have any ambiguity?








From: Std-Proposals <std-proposals-bounces_at_[hidden]> on behalf of organicoman via Std-Proposals <std-proposals_at_[hidden]>
Sent: Wednesday, July 31, 2024 10:23:07 PM
To: Sebastian Wittmeier via Std-Proposals <std-proposals_at_[hidden]>
Cc: organicoman <organicoman_at_yahoo.fr>
Subject: Re: [std-proposals] Function overload set type information loss


This is my proposal(C++17 based) hopefully somebody will rewrite it in a better standard way.
----------------------------------------------------



Operator
   effdecltype(expression)


Other plausible name : typeof (expr)


Intro
    C++ Is a type strong programming language, each object used has a well known type.
For all object instantiated/defined in a program there is a one to one mapping between the name and the type it belongs to.
Except for 3 categories:
1- Arrays
2- Function templates
3- Variable templates


Collapsing type
signature into one type id, makes tracing back what was passed/called impossible. And it is an aberration to the strong typing adopted by C++ language.
Let's illustrate that with some examples:
Category 1:


#include <type_traits>
template<typename T>
void foo(T t)
{
    static_assert(std::is_same_v<T, char*>);
}
int main()
{
   char arr[5];
   foo(arr); // #1 assertion passed
   static_assert(std::is_same_v<decltype(arr),
        char[5]>);
// #2 assertion passed
}


In the example above, the same variable
arr has two different types
#1 char*
#2 char[5]
That's what i mean by type signature collapsing. The standard calls it
array pointer decay.


Category 2:


#include<type_traits>
template<typename T>
void foo() { }


int main()
{
  auto foo_1 = &foo<int>;
  auto foo_2 = &foo<double>;
  static_assert
  (
      std::is_same_v
       <
          decltype(foo_1)
        , decltype(foo_2)
>
   ); // #1 assertion passed
}


In this example, we have two functions instances with different
type signature, yet their type id is the same.


Category 3:


#include<type_traits>
template<typename T>
int Var = 123;


int main()
{
  auto var_1 = Var<double>;
  auto var_2 = Var<char>;
  static_assert
  (
    std::is_same_v
    <
      decltype(var_1)
    , decltype(var_2)
>
   ); // #1 assertion passed
}


The same as well for variable templates.
Different signature collapsed to same
type id.


The standard doesn't explain why it adopted this behavior, except for the case of array pointer decay.
But from the examples above, we can observe a recurring pattern.


There are two different types, one written by the user, and one seen by the compiler.


The type written by the user is the
real
effective type, but the type seen by the compiler is the
apparent type over the rest of the program.


Motivation
    To preserve the one to one mapping between what the user intend and what the program see, and to keep a way to trace back all calls and usage of any type, we need to solve this duality between
effective type and
apparent type.
Some consequences engendered from this solution, is type introspection and easy type erasure.


Discussion
    While analyzing the behavior of the examples given (category 2, category 3). We extracted the following observations:


1- if the template parameters don't participate in the
apparent type of the object, a dissociation between the
apparent type and the effective type is created.
Let us call this kind of template parameter an
othogonal template paramter


2- All orthogonal template parameters contribute in resolving the instance's value, but don't participate in the type id.


So mathematically speaking:


let:
template<typename...Ts> U;
a templated type where:
U ∈ { category2, category3 }
let:
S(U) = {Ts...} the set of all template arguments of U.
If we prove that there is a sub set A(U) of S(U), where:
A(U) ⊆ S(U)
^ A(U) is not empty.
∀P ∈ A(U)
^ P is an othogonal template paramter.
then:
apparent type(U) is Not the same as effective type(U).


Some examples for illustration:


#include <type_traits>
template<typename T, typename V>
void foo(V v) { }


int main()
{
   auto f_ptr1= &foo<int, double>;
   auto f_ptr2= &foo<char, double>;
   static_assert(std::is_same_v<decltype(f_ptr1), decltype(f_ptr2)>); //
assertion passed.
}


In the above example:
S(U) = { T, V }
A(U) = { T }


A(U) is not empty which implys that:
effective type (foo) != apparent type (foo);




Introducing operator effdecltype (or typeof)
    The purpose of this operator is to detect the case described above and record it for the concerned variable, and avoid breaking any code relying on apparent type.


Definitions
    This operator has the following properties.


Let
expr,
expr1, expr2 ∈ {category1, category2, category3}


1- If:
effdecltype(expr1)
same as effdecltype(expr2)
Then:
decltype(expr1)
same as decltype(expr2)


2- casting
decltype(effdecltype(expr))
same as decltype(expr);
Given this property we can write directly
decltype(expr) same as effdecltype(expr)
for convenience.


3- If:
decltype(expr1)
same as decltype(expr2)
And
effdecltype(expr1)
not as effdecltype(expr2)
Then:
effdecltype(expr1)
could be written as
decltype (expr1)
<A1(U)>
And:
effdecltype(expr2)
could be written as
decltype (expr2)
<A2(U)>
Where:
A1(U)
and A2(U)
are the set of orthogonal template parameters for
expr1
and expr2
respectively.


4- change of behavior for
auto keyword :
auto var = effdecltype(expr);
auto var2 =
expr;


auto here carries the same meta-data as effdecltype(expr),
denoting perfect capturing of the
effective type.


5- casting:
decltype(expr)
var = effdecltype(expr){
expr};


This is an explicit cast of the
effective type to an apparent type, and the
var object type has no extra meta-data.


6- value comparison:
if:
effdecltype(expr1){ args } == effdecltype(expr2){
 args }
then:
decltype(expr1){ args } == decltype(expr2)
 { args };
And the opposite is also true.
Where:
args are the arguments to the constructor of the expression's type, which are usually the expression itself.


7- fallback to decltype behavior:
If exprX is not one of the 3 categories above.
Then:
effdecltype(exprX) same as
decltype(exprX)


In funny summary:
effdecltype is
decltype with X-ray vision


Illustration with examples:
(If effdecltype operator is implemented)


#include <type_traits>
template<typename T, typename V>
void foo(V v) { }


template<typename X, typename Y>
auto Var = Y{123};


void
  bar(effdecltype(Var<float, short>) const& a)
{
    static_assert(std::is_same_v<short,
decltype(a)>); // assertion passed


   static_assert
   (
      std::is_same_v
      <
         effdecltype(a)
       , effdecltype(Var<double,short>)
      >
    );//passed (must read comment
X*)
}


template<typename X>
using meta_double = effdecltype(Var<X,double>);


int main()
{
  auto foo_1 = effdecltype(&foo<int, double>){&foo<int, double>}; //#1


  void(*foo_2)(char) = effdecltype(&foo<double, char>){ &foo<double, char>}; //#2


  auto foo_3 = &foo<long long, char>; //
#3


  static_assert
  (
    std::is_same_v
    <
      decltype(foo_3)
    , effdecltype(foo_3)
>
   ); //
assertion passed (read comment X*)


  auto var_1 = effdecltype(Var<char, int>){ Var<char, int>};
  int var_2 = Var<double, int>;


  if(var_1 == var_2)
    puts("different effective types, same value"); //printed


  static_assert
  ( 
    std::is_same_v
    <
    decltype(var_1)
       , effdecltype(var_1)
>
   ); // assertion passed (read comment X*)
  
   meta_double meta1 = Var<char,double>; //
#4
   meta_double<float> meta2 = Var<float,double>; // #5
}


In the example above we illustrated some of the properties of operator
effdecltype


In effdecltype's type notation we can write


Case#1: foo_1 is void(*)<int>(double), perfect capture with operator
auto.


Case#2: foo_2 is void(*)(char), casting from effective to apparent type.


Case#3: foo_3 is void(*)<long long>(char), perfect capture with operator
auto.


Case#4: meta1 is (double)<char>,
First effdecltype was applied to the right hand side of the assignement to deduce the missing
orthogonal template parameter of the alias type
meta_double, via template argument deduction mechanism, then the value of
Var is assigned to
meta1


Case#5: meta2 is (double)<float>,
Same as #4 but the othogonal template paramter was explicitly provided, if types mismatch a compiler warning is raised.
The decision to raise a warning instead of an error, is to denote that, what the user writes is what the program should adopt.


Comment X*:
Since the actual type-traits machinery relys on the
apparent type to do its job, then when we use
effdecltype inside any type-traits meta function the
effective type is cast to the apparent type. Remember
effdecltype is just
decltype with X-ray vision.
Later in the this paper we will show how to fix this situation.


Usage
    After defining what the operator does, we have the opportunity, in this part, to show some usage and implications of such operator.


To use the operator effdecltype effectively, we need to add some tools to our beloved C++ language.


builtin functions:
template<typename U>
constexpr size_t __orthogonal_size__(effdecltype(U{}) = {}) noexpect;


Returns the count of orthogonal template parameters (size of
A(U) defined above.)


template<typename U, typename R, size_t I>
constexpr R
__ortho_type_at__<I>(effdecltype(U{}) = {}) noexcept;


Extracts the orthogonal template parameter at index
I in the set A(U) defined above.


More builtin functions can be defined, but this is what i have in mind now.


Tips and tricks:
The following is not an exhaustive list of all what we can do with this operator, but just a guid to unleash the user's imagination.


1-store and load types:


#include <vector>
template<typename T>
inline constexpr int Var= 123;


struct S{
    template<typename T>
    using meta_int = effdecltype(Var<T>);
meta_int m_val;
    template<typename T>
    constexpr S() noexcept
      : m_val (Var<T>)
    { }
};


template<typename T>
void ActBasedOnType(int val)
{
  std::cout << typeid(T).name() << "->" << val << std::endl;
}
int main()
{
  std::vector<S> vec;
  vec.push_back(S<int>{} );
  vec.push_back(S<double>{} );
  vec.push_back(S<char>{} );


  for(const auto& elem: vec)
  {
    std::cout << S.m_val << std::endl;
  } // prints:
123\n123\n123\n


  for(const auto& elem: vec)
  {
     auto data = S.s; // uses effdecltype
     ActBasedOnType<__ortho_type_at__<0>(data)>(data);
  }//
prints: int->123\ndouble->123\nchar->123\n
}


So what is all this black magic??
Explanation:
Observation: You must keep in mind that the 3 categories we are dealing with are global constexpr variables. And when we assign their values to other variables
 we can decay it to a reference (case of function pointer) if that variable doesn't modify its value, or we stamp a copy of their values (for variable templates if copy needed).
Our interest is in the orthogonal template parameters not the value.
With this paper addition we are going to exploit that fact deeply.


The struct S above defines a templated effective type alias, captured from the global variable Var<T>.


We define also a function template which prints the type of its template argument.


Inside main we creat a vector of S.
sizeof(S) == sizeof(int) and alignof(S) == alignof(int)
Also, important to notice, is that vector is constexpr vector even we didn't explicity wrote it.
Why? Because all elements of vec are references to a global constexpr variable known at compile time and its size is known at compile time.


We push values of type S, using the templated constructor.
Since we are instantiating values of
Var<T> indirectly, the compiler creates an internal table of mapping between orthogonal template parameter T and an index i in vec.


In the first "for loop", the regulare compilation process is performed.
m_val is of
int type no magic there.


In the second "for loop", the compiler unrolls the loop and instantiates a function out of
ActBasedOnType<T> with
T fetched from the internal structure created when pushing values into the vector.
So the loop will be replaced by


ActBasedOnType<int>(123);
ActBasedOnType<double>(123);
ActBasedOnType<char>(123);


The compiler has the choice to keep that inner mapping structure for latter use, and it can update the mapping if it sees necessary.
Otherwise it can destroy it if it can prove that there is no necessity to keep it.


All the magic happens in the unrolling of the loop.
Because all information are available at compile time. (Remember the green observation above.)
Unrolling the loop is one technique , other techniques could be stamping a switch statment, or implementing a visitation method.... compilers implementers are free to amaze us.


FIN.




Oh man it's tiresome on a cellphone..🥵








-- 
Std-Proposals mailing list
Std-Proposals_at_[hidden]
https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals

Received on 2024-07-31 22:13:48