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 00:53:10 +0400
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_[hidden] 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_[hidden]>
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..🥵






Received on 2024-07-31 20:53:25