relaxing rules for ternary operator. Allow incompatible types.

253 views
Skip to first unread message

ma.ka...@web.de

unread,
May 20, 2017, 1:41:57 PM5/20/17
to ISO C++ Standard - Future Proposals
Hi Guys,

I had the following situation:

I have a Group of StaticSet classes with a compatible interface but distinct in there concrete type. empty_set is not an intervall!
I tried the following:


auto dec_digit = make_interval('0', '0' + std::min(base, DECIMAL_BASE) - 1);
auto upper_digit = (base > DECIMAL_DIGIT) ? make_interval('A', 'A' + (base - 1 - DECIMAL_BASE)) : empty_set;
auto lower_digit = (base > DECIMAL_DIGIT) ? make_interval('a', 'a' + (base - 1 - DECIMAL_BASE)) : empty_set;


Wouldn't it be possible to allow such code, especially in conjunction with concepts?

MFG

Martin Kalbfuß

Thiago Macieira

unread,
May 20, 2017, 3:29:56 PM5/20/17
to std-pr...@isocpp.org
No. lower_digit must have exactly one type, determined at compile-time. What
you're asking for would cause the type to be determined at runtime, which is
not possible in C++.

Alternatives:
* convert empty_set to make_interval's type
* convert either to a common type (which you need to be explicit about, so it
won't work with auto)
* use std::variant<interval, decltype(empty_set)>

--
Thiago Macieira - thiago (AT) macieira.info - thiago (AT) kde.org
Software Architect - Intel Open Source Technology Center

ma.ka...@web.de

unread,
May 20, 2017, 3:45:26 PM5/20/17
to ISO C++ Standard - Future Proposals, ma.ka...@web.de
Thanks for your answer. I see the problem now. The condition is a runtime condition. So this isn't going to work. But wouldn't it be possible for a constexpr condition?

for Example:

auto x = (std::is_integral_v<T>) ? A{} : B{};



This would be like if constexpr with implicit constexpr determinded by the compiler.

Marc Mutz

unread,
May 20, 2017, 4:14:48 PM5/20/17
to std-pr...@isocpp.org
On 2017-05-20 21:45, ma.ka...@web.de wrote:
> Thanks for your answer. I see the problem now. The condition is a
> runtime condition. So this isn't going to work. But wouldn't it be
> possible for a constexpr condition?
>
> for Example:
>
> auto x = (std::is_integral_v<T>) ? A{} : B{};
>
> This would be like if constexpr with implicit constexpr determinded by
> the compiler.

I had the same idea the other day :)

constexpr-if is explicit, why should the the ternary be different?

auto x = std::is_integral_v<T> constexpr ? A{} : B{};

Thanks,
Marc

Zhihao Yuan

unread,
May 20, 2017, 5:01:33 PM5/20/17
to std-pr...@isocpp.org

On Sat, May 20, 2017 at 3:11 PM, Marc Mutz <marc...@kdab.com> wrote:

auto x = (std::is_integral_v<T>) ? A{} : B{};

This would be like if constexpr with implicit constexpr determinded by
the compiler.

I had the same idea the other day :)

constexpr-if is explicit, why should the the ternary be different?

  auto x = std::is_integral_v<T> constexpr ? A{} : B{};

Seems not so hard?

  auto x = std::conditional_t<std::is_integral_v<T>, A, B>{};

--
Zhihao Yuan, ID lichray
The best way to predict the future is to invent it.
_______________________________________________

Nicol Bolas

unread,
May 20, 2017, 5:49:54 PM5/20/17
to ISO C++ Standard - Future Proposals, z...@miator.net
On Saturday, May 20, 2017 at 5:01:33 PM UTC-4, Zhihao Yuan wrote:

On Sat, May 20, 2017 at 3:11 PM, Marc Mutz <marc...@kdab.com> wrote:

auto x = (std::is_integral_v<T>) ? A{} : B{};

This would be like if constexpr with implicit constexpr determinded by
the compiler.

I had the same idea the other day :)

constexpr-if is explicit, why should the the ternary be different?

  auto x = std::is_integral_v<T> constexpr ? A{} : B{};

Seems not so hard?

  auto x = std::conditional_t<std::is_integral_v<T>, A, B>{};


If all you want to do is select a type, then it's not hard. If you want to do what the OP suggested (ignoring the fact that his condition was probably not `constexpr`), that not something you can do with such a tool.

Mingxin Wang

unread,
May 20, 2017, 10:54:38 PM5/20/17
to ISO C++ Standard - Future Proposals, z...@miator.net
If the condition is not "constexpr",  and the types are determined at runtime, the "proxies" and the "wrappers" can be used to solve this problem. (https://groups.google.com/a/isocpp.org/forum/#!topic/std-proposals/kvkgsHM6wFQ)

ma.ka...@web.de

unread,
May 21, 2017, 4:31:31 AM5/21/17
to ISO C++ Standard - Future Proposals
introducing constexpr ternary seems like a natural extension.

Explicitly denoting constexpr is a posibility. But i vote against it.
One point of the ternary operator is its shortness in comparison to if else. Adding an additional constexpr increases the statemanent size.
They tend to get unreadable fastly, anyway. An additional constexpr makes things worse.
Another point is the position of the constexpr keyword. Where should it go? There are at least 3 reasonable positions I can think of.

ma.ka...@web.de

unread,
May 21, 2017, 5:03:45 AM5/21/17
to ISO C++ Standard - Future Proposals
I revisited your statement, that this isn't possible, because the type would be determined at runtime. I have to disagree, now.  It is possible at compile time.

Look at the following code:

auto x = runtime_condition ? A{} : B{};
x
.doit();

This is not going to work, as you stated. But you could write the following instead:

auto f = [&](auto x) { x.doit(); };
runtime_condition
? f(A{}) : f(B{});

The code, following the ternary statement is moved into a generic lambda.
auto x = runtime_condition ? A{} : B{};

The generic lambda is instantiated two times, for each branch. Depending on runtime_condition,
only one instance is executed. The statement

auto x = runtime_condition ? A{} : X{};

could introduce the following code inside the current scope twice, like the generic lambda did. Ones for each type.
Depending on the runtime_condition, the code pointer is moved to the first or the second instantiaton with a jump.

The following C++ code snippet describes roughly how the generated code could look like.

#include <iostream>

struct A{ void doit(){ std::cout << "A::doit" << std::endl; } };
struct B{ void doit(){ std::cout << "B::doit" << std::endl; } };

int main()
{
   
auto runtime_condition = true;

   
if(runtime_condition)
       
goto ternary_branch_1;
   
else
       
goto ternary_branch_2;

ternary_branch_1
:
    A __instance1__
;
    __instance1__
.doit();
   
return 0;
ternary_branch_2
:
    B __instance2__
;
    __instance2__
.doit();
   
return 0;
}

ma.ka...@web.de

unread,
May 21, 2017, 5:15:21 AM5/21/17
to ISO C++ Standard - Future Proposals, ma.ka...@web.de
The coresponding assembly code without iostream to avoid unecessary complexity:

A::doit():
        push    rbp
        mov     rbp
, rsp
        mov     QWORD PTR
[rbp-8], rdi
        nop
        pop     rbp
        ret
B
::doit():
        push    rbp
        mov     rbp
, rsp
        mov     QWORD PTR
[rbp-8], rdi
        nop
        pop     rbp
        ret
main
:
        push    rbp
        mov     rbp
, rsp
       
sub     rsp, 16
        mov     BYTE PTR
[rbp-1], 1
        cmp     BYTE PTR
[rbp-1], 0
        je      
.L9
        nop
        lea     rax
, [rbp-2]
        mov     rdi
, rax
        call    A
::doit()
        mov     eax
, 0
        jmp    
.L8
.L9:
        nop
        lea     rax
, [rbp-3]
        mov     rdi
, rax
        call    B
::doit()
        mov     eax
, 0
.L8:
        leave
        ret

Mingxin Wang

unread,
May 21, 2017, 9:00:03 AM5/21/17
to ISO C++ Standard - Future Proposals, ma.ka...@web.de
You may as well consider the following situations:

Loops

It is difficult for compilers to generate code for loops with the feature you proposed, e.g.:

void foo(int n) {
  /* A: Do something before the loop */
  for (int i = 0; i < n; ++i) {
    /* B: Do something */
    auto x = runtime_condition() ? A{} : B{};
    /* C: Do some other things with x */
  }
  /* D: Do something after the loop */
}
Code Snippet 1

As far as I am concerned, here is the equivalent of the code that the compiler would automatically generate for the code above (with your method):

template <class T>
void bar(int i, int n, T x) {
  /* C: Do some other things with x */
  ++i;
  if (i < n) {
    /* B: Do something */
    if (runtime_condition()) {
      bar(i, n, A{});
    } else {
      bar(i, n, B{});
    }
  }
}

void foo(int n) {
  /* A: Do something before the loop */
  if (0 < n) {
    /* B: Do something */
    if (runtime_condition()) {
      bar(1, n, A{});
    } else {
      bar(1, n, B{});
    }
  }
  /* D: Do something after the loop */
}
Code Snippet 2

It is too complicated! Moreover, as the recursive call of the "function template bar" is not able to be optimized, it will possibly increase the runtime overhead.

Size

Compilers may generate a lot of redundant code when the feature is used extensively, e.g.:

auto x0 = runtime_condition_0() ? A0{} : B0{};
auto x1 = runtime_condition_1() ? A1{} : B1{};
// ...
auto xn = runtime_condition_n() ? An{} : Bn{};
Code Snippet 3

As you introduced, the compiler is supposed to generate code for every path! For the code above, it requires O(2^n) size for code generation, which is NOT acceptable in most cases.

In fact, the challenges above can be solved gracefully with the "proxies" and the "wrappers".

As the condition is determined at runtime, the concrete type cannot be deduced at compile time. The only reason for us to save the returned value is that they have similar expressions and semantics. We can simply extract the common expressions of the types which are used in the context to a "proxy", and save the returned value with a "wrapper", e.g. Code Snippet 1 can be reconstructed as is shown below (with better performance and smaller size):

/* Declares the common expressions required in the context */
proxy P {
  void f_0();
  double f_1(int, int);
};

void foo(int n) {
  /* A: Do something before the loop */
  for (int i = 0; i < n; ++i) {
    /* B: Do something */
    Wrapper<P> x(runtime_condition() ? Wrapper<P>(A{}) : Wrapper<P>(B{}));
    P p = x.get_proxy();
    /* C: Do some other things with p */
  }
  /* D: Do something after the loop */
}
Code Snippet 4

Mingxin Wang

Nicol Bolas

unread,
May 21, 2017, 9:59:43 AM5/21/17
to ISO C++ Standard - Future Proposals, z...@miator.net
On Saturday, May 20, 2017 at 10:54:38 PM UTC-4, Mingxin Wang wrote:
If the condition is not "constexpr",  and the types are determined at runtime, the "proxies" and the "wrappers" can be used to solve this problem. (https://groups.google.com/a/isocpp.org/forum/#!topic/std-proposals/kvkgsHM6wFQ)

So long as you're willing to suffer through needless virtual call overhead, as well as less obvious code, yes.
Message has been deleted

ma.ka...@web.de

unread,
May 21, 2017, 10:11:14 AM5/21/17
to ISO C++ Standard - Future Proposals, ma.ka...@web.de
The following code should be generated when a loop is involved. the loop has its own scope. The code duplication is limited to the scope of the loop.
But let's improve it further. Instead of duplicate the following code in the enclosing scope, it is sufficient to duplicate the dependend code.
Code is dependend  if the result of the ternary operation is involved. So only these parst are duplicated. As an addition, the code is only duplicated,
if the condition is known at runtime. If it is known at compile time, only one branch is created. Code outside of the loop or the code necessary to
implement the loop is NOT duplicated and is not part of the branches. For now, I will call this construct "generic ternary operator". Like other generics
it introduces code duplication. It's not a special problem of this construct.
It is important, that existing code does not break. For cases where the existing conversion rules apply, there should be no duplication.

Exmaple including a loop:

struct A{ void doit(){ } };
struct B{ void doit(){ } };


int main()
{
   
auto runtime_condition = true;


   
for(int i = 0; i < 100; i++) {

       
auto x = runtime_condition ? A{} : B{};
        x
.doit();

       
/* do something without x */
        x
.doit()
       
/* do something without x */
   
}

   
return 0;
}




struct A{ void doit(){ } };
struct B{ void doit(){ } };


int main()
{
   
auto runtime_condition = true;


   
for(int i = 0; i < 100; i++) {

       
if(runtime_condition)
           
goto ternary_branch_1;
       
else
           
goto ternary_branch_2;

ternary_branch_1
:
        A __instance1__
;
        __instance1__
.doit();

       
goto ternary_end_1;
ternary_branch_2
:
        B __instance2__
;
        __instance2__
.doit();
ternary_end_1
:        
       
/* do something without x */;
ternary_branch_3
:
        __instance1__
.doit();
       
goto ternary_end_2;
ternary_branch_4
:
       __instance2__
.doit();
ternary_end_2
:
       
/* do something without x */;
   
}

   
return 0;
}




A::doit():
        push    rbp
        mov     rbp
, rsp
        mov     QWORD PTR
[rbp-8], rdi
        nop
        pop     rbp
        ret
B
::doit():
        push    rbp
        mov     rbp
, rsp
        mov     QWORD PTR
[rbp-8], rdi
        nop
        pop     rbp
        ret
main
:
        push    rbp
        mov     rbp
, rsp
       
sub     rsp, 16

        mov     BYTE PTR
[rbp-5], 1
        mov     DWORD PTR
[rbp-4], 0
.L11:
        cmp     DWORD PTR
[rbp-4], 99
        jg      
.L4
        cmp     BYTE PTR
[rbp-5], 0
        je      
.L13
        nop
        lea     rax
, [rbp-6]

        mov     rdi
, rax
        call    A
::doit()

        jmp    
.L9
.L13:
        nop
        lea     rax
, [rbp-7]

        mov     rdi
, rax
        call    B
::doit()
.L9:
        lea     rax
, [rbp-6]

        mov     rdi
, rax
        call    A
::doit()

        nop
        add     DWORD PTR
[rbp-4], 1
        jmp    
.L11
.L4:
        mov     eax
, 0
        leave
        ret

Nicol Bolas

unread,
May 21, 2017, 10:11:33 AM5/21/17
to ISO C++ Standard - Future Proposals, ma.ka...@web.de
On Sunday, May 21, 2017 at 5:03:45 AM UTC-4, ma.ka...@web.de wrote:
I revisited your statement, that this isn't possible, because the type would be determined at runtime. I have to disagree, now.  It is possible at compile time.

I question whether the value of such a construct is worth the code readability issues. You're effectively creating template code within existing code, arbitrarily. You're making it difficult for a user to track down the type of an object.

Since `auto` and `decltype` have become standard C++, there has been a move away from easily being able to determine the type of an object. And in many cases, it's fine that we don't see the type spelled out, because it's obvious by inspection what it is. But once you start saying that an object's type could be expression A or expression B or expression C, etc, it becomes difficult to follow the logic of our code.

Considering the potential downside in making code easily follow-able, I would like to see some evidence that people frequently use idioms like:


auto f = [&](auto x) { x.doit(); };
runtime_condition
? f(A{}) : f(B{});

Because if they don't, then the feature's motivation is weak.

Alternatively, I would say that it needs to be a different ternary expression. Even if it's something as simple as `??` rather than `?`, it would at least clue us in as to exactly when a user is going to do something like that.

It should also be noted that this is coming very close to having full-on language support for variant types. Which really means pattern matching and such. Perhaps there's a better feature deeper down here that could handle this in a different way.

Mingxin Wang

unread,
May 21, 2017, 10:34:08 AM5/21/17
to ISO C++ Standard - Future Proposals, z...@miator.net
If the type cannot be deduced at compile time, then runtime overhead is inevitable.

Nicol Bolas

unread,
May 21, 2017, 12:40:19 PM5/21/17
to ISO C++ Standard - Future Proposals, z...@miator.net
On Sunday, May 21, 2017 at 10:34:08 AM UTC-4, Mingxin Wang wrote:
If the type cannot be deduced at compile time, then runtime overhead is inevitable.
 
And yet, the OP just outlined an implementation that has zero runtime overhead. Or at least trivial overhead: code size and possibly stack storage. There would be no runtime performance overhead outside of the initial conditional branch.

Which would be far faster than a virtual function call, or any other form of type-erasure.

Thiago Macieira

unread,
May 21, 2017, 1:08:21 PM5/21/17
to std-pr...@isocpp.org
On domingo, 21 de maio de 2017 02:03:45 PDT ma.ka...@web.de wrote:
> The code, following the ternary statement is moved into a generic lambda.
> auto x = runtime_condition ? A{} : B{};
>
> The generic lambda is instantiated two times, for each branch. Depending on
> runtime_condition,
> only one instance is executed. The statement

You do realise this grows exponentially, right?

auto x = runtime_condition ? A{} : B{};
auto y = runtime_condition ? C{} : D{};
auto z = runtime_condition ? E{} : F{};
x.f() + y.f() + z.f();

There are 8 different branches there to make the above work.

Thiago Macieira

unread,
May 21, 2017, 1:17:03 PM5/21/17
to std-pr...@isocpp.org
On domingo, 21 de maio de 2017 10:08:16 PDT Thiago Macieira wrote:
> You do realise this grows exponentially, right?
>
> auto x = runtime_condition ? A{} : B{};
> auto y = runtime_condition ? C{} : D{};
> auto z = runtime_condition ? E{} : F{};

I mean different runtime conditions here.

Nicol Bolas

unread,
May 21, 2017, 1:35:10 PM5/21/17
to ISO C++ Standard - Future Proposals
On Sunday, May 21, 2017 at 1:08:21 PM UTC-4, Thiago Macieira wrote:
On domingo, 21 de maio de 2017 02:03:45 PDT ma.ka...@web.de wrote:
> The code, following the ternary statement is moved into a generic lambda.
> auto x = runtime_condition ? A{} : B{};
>
> The generic lambda is instantiated two times, for each branch. Depending on
> runtime_condition,
> only one instance is executed. The statement

You do realise this grows exponentially, right?

        auto x = runtime_condition ? A{} : B{};
        auto y = runtime_condition ? C{} : D{};
        auto z = runtime_condition ? E{} : F{};
        x.f() + y.f() + z.f();

There are 8 different branches there to make the above work.

This is one of the reasons why I think, if we're going to do this, then it needs to be a different operator from ?:. Something where you can clearly see that you're invoking this implicit template stuff.

And like I said elsewhere, I get the feeling that there is more to this implicit template instantiation than there first appears. This seems almost like pattern matching, but done implicitly via template instantiation, rather than explicitly through some kind of switch statement.

ma.ka...@web.de

unread,
May 21, 2017, 4:54:52 PM5/21/17
to ISO C++ Standard - Future Proposals
Yeah, i realize that. This is something you have to consider if you're using templates extensively. This is not the right solution for everything. I think in most cases, you use one or at most two ternary operators in one place. For three or more ternary operators the runtime overhead is assumably the lesser of two evils. But even if it grows exponentially, the constants are pretty small. Normaly only a few instructions are involved.

ma.ka...@web.de

unread,
May 21, 2017, 5:03:05 PM5/21/17
to ISO C++ Standard - Future Proposals
Mabe you're right. Another operator could be a better choice. But I'm still unsure about it. Another approach is to use an attribute to disallow genericity.


auto x = runtime_condition [[non generic]] ? A{} : B{};

or vice versa

auto x = runtime_condition [[generic]] ? A{} : B{};



It would be interesting to know, if there are other places where the same technique could be applied. Maybe for different return types of function calls, depeding on the passed arguments. I have to explore that.

Thiago Macieira

unread,
May 21, 2017, 8:12:20 PM5/21/17
to std-pr...@isocpp.org
On domingo, 21 de maio de 2017 13:54:51 PDT ma.ka...@web.de wrote:
> Yeah, i realize that. This is something you have to consider if you're
> using templates extensively.

No, it isn't. Today, the use of templates does not imply exponential growth of
generated code.

That is something completely new with your proposal.

> This is not the right solution for everything.
> I think in most cases, you use one or at most two ternary operators in one
> place. For three or more ternary operators the runtime overhead is
> assumably the lesser of two evils. But even if it grows exponentially, the
> constants are pretty small. Normaly only a few instructions are involved.

Not necessarily. Remember that you may not reevaluate the condition, so either
this generates completely separate code paths (and thus the exponential
expansion) or the result of the evaluation is stored in a hidden variable that
is evaluated every time you access that automatic variable.

That is, when you write:

auto x = runtime_condition1 ? A{} : B{};
auto y = runtime_conditoin2 ? C{} : D{};
x.f();

This expands to either:

if (runtime_condition1) {
auto x = A{};
if (runtime_condition2) {
auto y = C {};
x.f() + y.f();
} else {
auto y = D {};
x.f() + y.f();
}
} else {
auto x = B{};
if (runtime_condition2) {
auto y = C {};
x.f() + y.f();
} else {
auto y = D {};
x.f() + y.f();
}
}

or to:

variant<A, B> x;
variant<C, D> y;
bool hidden1 = runtime_condition1;
if (hidden1)
x.create<A>();
else
x.create<B>();
bool hidden2 = runtime_condition2;
if (hidden2)
y.create<C>();
else
y.create<D>();
if (hidden1) {
if (hidden2)
x.as<A>().f() + y.as<C>().f();
else
x.as<A>().f() + y.as<D>().f();
} else {
if (hidden2)
x.as<B>().f() + y.as<C>().f();
else
x.as<B>().f() + y.as<D>().f();

ma.ka...@web.de

unread,
May 22, 2017, 2:33:33 AM5/22/17
to ISO C++ Standard - Future Proposals
Sure, you don't have always exponential growth when using templates. But if you simulating this case with template functions you end up having the same problem.
So I wouldn't say, that this something completely new. It is the nature of the problem we try to solve. You have either runtime overhad or exponential growth.
Maybe there is a way to avoid exponential growth using optimization techniques. Assumably in many cases the optimizer will reduce the amount of code anyway.
There could be a guaranteed optimization. But I'm not sure which. I will build up an example, where the code isn't completely optimized away.

I assume variant is an untagged union? Let's try to minimise our examples.
Message has been deleted
Message has been deleted

ma.ka...@web.de

unread,
May 22, 2017, 3:19:35 AM5/22/17
to ISO C++ Standard - Future Proposals, ma.ka...@web.de
struct W{ int doit(){ return 1; } };
struct X{ int doit(){ return 2; } };
struct Y{ int doit(){ return 3; } };
struct Z{ int doit(){ return 4; } };


int example(bool runtime_condition1, bool runtime_condition2) {
   
int result;

   
if(!runtime_condition1)
       
goto ternary_branch_1;

   
if(!runtime_condition2)
       
goto ternary_branch_2;

    result
= W{}.doit() + Y{}.doit();
   
goto ternary_end;
   
ternary_branch_1
:

   
if(!runtime_condition2)
       
goto ternary_branch_3;

    result
= X{}.doit() + Y{}.doit();
   
goto ternary_end;

ternary_branch_2
:

    result
= W{}.doit() + Z{}.doit();
   
goto ternary_end;

ternary_branch_3
:

    result
= X{}.doit() + Z{}.doit();

ternary_end
:
   
return result;
}

with -O0:

W
::doit():

        push    rbp
        mov     rbp
, rsp
        mov     QWORD PTR
[rbp-8],
rdi
        mov     eax
, 1
        pop     rbp
        ret
X
::doit():

        push    rbp
        mov     rbp
, rsp
        mov     QWORD PTR
[rbp-8],
rdi
        mov     eax
, 2
        pop     rbp
        ret
Y
::doit():

        push    rbp
        mov     rbp
, rsp
        mov     QWORD PTR
[rbp-8],
rdi
        mov     eax
, 3
        pop     rbp
        ret
Z
::doit():

        push    rbp
        mov     rbp
, rsp
        mov     QWORD PTR
[rbp-8],
rdi
        mov     eax
, 4
        pop     rbp
        ret
example
(bool, bool):
        push    rbp
        mov     rbp
, rsp
        push    rbx
       
sub     rsp, 40
        mov     edx
, edi
        mov     eax
, esi
        mov     BYTE PTR
[rbp-36], dl
        mov     BYTE PTR
[rbp-40], al
        movzx   eax
, BYTE PTR [rbp-36]
        xor     eax
, 1
        test    al
, al
        jne    
.L21
        movzx   eax
, BYTE PTR [rbp-40]
        xor     eax
, 1
        test    al
, al
        jne    
.L22
        lea     rax
, [rbp-28]
        mov     rdi
, rax
        call    W
::doit()
        mov     ebx
, eax
        lea     rax
, [rbp-27]
        mov     rdi
, rax
        call    Y
::doit()
        add     eax
, ebx
        mov     DWORD PTR
[rbp-20], eax
        jmp    
.L14
.L21:
        nop
        movzx   eax
, BYTE PTR [rbp-40]
        xor     eax
, 1
        test    al
, al
        jne    
.L23
        lea     rax
, [rbp-26]
        mov     rdi
, rax
        call    X
::doit()
        mov     ebx
, eax
        lea     rax
, [rbp-25]
        mov     rdi
, rax
        call    Y
::doit()
        add     eax
, ebx
        mov     DWORD PTR
[rbp-20], eax
        jmp    
.L14
.L22:
        nop
        lea     rax
, [rbp-24]
        mov     rdi
, rax
        call    W
::doit()
        mov     ebx
, eax
        lea     rax
, [rbp-23]
        mov     rdi
, rax
        call    Z
::doit()
        add     eax
, ebx
        mov     DWORD PTR
[rbp-20], eax
        jmp    
.L14
.L23:
        nop
        lea     rax
, [rbp-22]
        mov     rdi
, rax
        call    X
::doit()
        mov     ebx
, eax
        lea     rax
, [rbp-21]
        mov     rdi
, rax
        call    Z
::doit()
        add     eax
, ebx
        mov     DWORD PTR
[rbp-20], eax
.L14:
        mov     eax
, DWORD PTR [rbp-20]
        add     rsp
, 40
        pop     rbx
        pop     rbp
        ret

with -O3:

example
(bool, bool):
        xor     eax
, eax
        test    sil
, sil
        sete    al
        test    dil
, dil
        je      
.L2
        add     eax
, 4
        ret
.L2:
        add     eax
, 5
        ret

Thiago Macieira

unread,
May 22, 2017, 11:19:09 AM5/22/17
to std-pr...@isocpp.org
On segunda-feira, 22 de maio de 2017 00:19:35 PDT ma.ka...@web.de wrote:
> example(bool, bool):
> xor eax, eax
> test sil, sil
> sete al
> test dil, dil
> je .L2
> add eax, 4
> ret
> .L2:
> add eax, 5
> ret

You're showing that it is possible to inline functions. We already knew that.

Now try this with non-inline doit() functions. Just remove the function bodies
from your example.

Bengt Gustafsson

unread,
May 22, 2017, 3:57:02 PM5/22/17
to ISO C++ Standard - Future Proposals, z...@miator.net
If we limit ourselves to the constexpr condition case I think this proposal has some merit as an if constexpr stateament can't be used to affect the type of a variable that outlives the if statement, which such a constexpr ? could.

A general non-constexpr version is in my opinion a far too long step away from the C++ type system, and also is very prone to hide type errors where you didn't want the compiler to do any trickery to counteract your mistyping, hiding bugs until later in the development cycle.

ma.ka...@googlemail.com

unread,
May 23, 2017, 4:07:52 AM5/23/17
to ISO C++ Standard - Future Proposals, z...@miator.net
The hiding of type errors and unwanted exponential growth of the code size could be avoided by using an attribute or a different operator symbol. Please ignore the attribute name and the choosen symbol for now. They are only placeholers.


auto x = runtime_condition [[generic]] ? A{} : B{};

or

auto x = runtime_condition ?? A{} : B{};

This way, the programmer has to tell the compiler that he want to use the generic version. No unexpected exponential growth and no hidden type errors. Maybe this is too much for the upcomming standard. There is no room for such an extension. The list of must have features is already too long. There is not much  room left besides Concepts and Modules, anyway. So this will be a proposal for a later standard. I think there are more cases, whrere this technique could be applied. These cases are not explored yet.

ma.ka...@googlemail.com

unread,
May 23, 2017, 4:22:01 AM5/23/17
to ISO C++ Standard - Future Proposals, z...@miator.net, ma.ka...@googlemail.com
I explored other situations where this could be applied.

//generic lambda
[[generic]] [](bool runtime_condition) {
   
if(runtime_condition) {
       
return A{};
   
} else {
       
return B{};
   
}
   
}

// generic function
[[generic]] auto doit(bool runtime_condition) {
   
if(runtime_condition) {
       
return A{};
   
} else {
       
return B{};
   
}    
}

// generic function with generic ternary operator
[[generic]] auto doit(bool runtime_condition) {
   
return runtime_condition [[generic]] ? A{} : B{};
}

ma.ka...@googlemail.com

unread,
May 23, 2017, 5:47:30 AM5/23/17
to ISO C++ Standard - Future Proposals, z...@miator.net, ma.ka...@googlemail.com
Further cases where this could be applied:

// generic variable
void doit(bool runtime_condition) {
   
[[generic]] auto x;

   
if(runtime_condition) {
        x
= A{};
   
} else {
        x
= B{};
   
}    
}

// generic variable with concept
void doit(bool runtime_condition) {
   
[[generic]] Iteratable x;

   
if(runtime_condition) {
        x
= A{};
   
} else {
        x
= B{};
   
}    
}

ma.ka...@googlemail.com

unread,
May 23, 2017, 5:51:22 AM5/23/17
to ISO C++ Standard - Future Proposals, z...@miator.net, ma.ka...@googlemail.com
And finally generic type definitions:

[[generic]] class X{};
[[generic]] class Y{};



Their use implies [[generic]] for all expressions. If all types in an expression are generic the expression itself is generic, too.

Thiago Macieira

unread,
May 23, 2017, 10:47:49 AM5/23/17
to std-pr...@isocpp.org
On Tuesday, 23 May 2017 01:07:52 PDT ma.kalbfuss via ISO C++ Standard - Future
Proposals wrote:
> The hiding of type errors and unwanted exponential growth of the code size
> could be avoided by using an attribute or a different operator symbol.
> Please ignore the attribute name and the choosen symbol for now. They are
> only placeholers.
>
> auto x = runtime_condition [[generic]] ? A{} : B{};
>
> or
>
> auto x = runtime_condition ?? A{} : B{};
>
> This way, the programmer has to tell the compiler that he want to use the
> generic version. No unexpected exponential growth and no hidden type
> errors. Maybe this is too much for the upcomming standard. There is no room
> for such an extension. The list of must have features is already too long.
> There is not much room left besides Concepts and Modules, anyway. So this
> will be a proposal for a later standard. I think there are more cases,
> whrere this technique could be applied. These cases are not explored yet.

You're missing the point that the code growth is not optional.

It doesn't matter how you mark this new syntax, the fact that we have a
variable whose type cannot be determined at compile time means that there's
runtime overhead.

With proper reflection, the generic type on the left side can be entirely
impemented in library code, with no compiler magic, with dispatcher functions
to A and B depending on the stored type. For example, it would generate:

template <typename A, typename B>
struct Generic
{
typeinfo *ti;
void *target;

Generic(A *object) : ti(typeinfo(A)), target(object) {}
Generic(B *object) : ti(typeinfo(B)), target(object) {}

int f()
{
return ti == typeinfo(A) ?
static_cast<A *>(target)->f() :
static_cast<B *>(target)->f();
}
};

Then if you have Generic<A, B> x, whenever you do:
x.f();

then the correct function is called.

But there was an overhead, since every call now checks the typeinfo. This
avoids the exponential growth of code, but it does increase the code
nonetheless and introduces a runtime overhead.

Nicol Bolas

unread,
May 23, 2017, 12:28:40 PM5/23/17
to ISO C++ Standard - Future Proposals


On Tuesday, May 23, 2017 at 10:47:49 AM UTC-4, Thiago Macieira wrote:
On Tuesday, 23 May 2017 01:07:52 PDT ma.kalbfuss via ISO C++ Standard - Future
Proposals wrote:
> The hiding of type errors and unwanted exponential growth of the code size
> could be avoided by using an attribute or a different operator symbol.
> Please ignore the attribute name and the choosen symbol for now. They are
> only placeholers.
>
> auto x = runtime_condition [[generic]] ? A{} : B{};
>
> or
>
> auto x = runtime_condition ?? A{} : B{};
>
> This way, the programmer has to tell the compiler that he want to use the
> generic version. No unexpected exponential growth and no hidden type
> errors. Maybe this is too much for the upcomming standard. There is no room
> for such an extension. The list of must have features is already too long.
> There is not much  room left besides Concepts and Modules, anyway. So this
> will be a proposal for a later standard. I think there are more cases,
> whrere this technique could be applied. These cases are not explored yet.

You're missing the point that the code growth is not optional.

It doesn't matter how you mark this new syntax, the fact that we have a
variable whose type cannot be determined at compile time means that there's
runtime overhead.

But that's not what you have. What you have is two different variables, which happen to have the same name. It turns this:

auto x = runtime_condition ?? expr1 : expr2;
x
.op1();
auto z = some_y;
x
.op2(&some_y);

into this:

if(runtime_condition)
{
 
auto x = expr1;
  x
.op1();
 
auto z = some_y;
  x
.op2(&some_y);
}
else
{
 
auto x = expr2;
  x
.op1();
 
auto z = some_y;
  x
.op2(&some_y);
}

The only runtime overhead is the overhead of the runtime condition. At no time after the `if` statement does the compiler check at runtime which type `x` happens to be.

You're right that the code growth is not optional. But the code growth is no different from any other template instantiation either.

Thiago Macieira

unread,
May 23, 2017, 12:51:23 PM5/23/17
to std-pr...@isocpp.org
On Tuesday, 23 May 2017 09:28:40 PDT Nicol Bolas wrote:
> But that's not what you have. What you have is *two different variables*,
> which happen to have the same name. It turns this:
>
> auto x = runtime_condition ?? expr1 : expr2;
> x.op1();
> auto z = some_y;
> x.op2(&some_y);
>
> into this:
>
> if(runtime_condition)
> {
> auto x = expr1;
> x.op1();
> auto z = some_y;
> x.op2(&some_y);
> }
> else
> {
> auto x = expr2;
> x.op1();
> auto z = some_y;
> x.op2(&some_y);
> }

That's only one of the possible expansions. The other is:

storage_area x;
if (runtime_condition) {
new (x) A(expr1);
x.as<A>().op1();
} else {
new (x) B(expr2);
x.as<B>().op1();
}
auto z = some_y;
if (runtime_condition)
x.as<A>().op2(&some_y);
else
x.as<B>().op2(&some_y);

> The only runtime overhead is the overhead of the runtime condition. At no
> time after the `if` statement does the compiler check at runtime which type
> `x` happens to be.

As I said, one of many possible implementations. As in my expansion, it may
want to duplicate the checking of the runtime condition (or save its result in
a hidden variable) so it can use a common code path that doesn't depend on the
runtime condition.

> You're right that the code growth is not optional. But the code growth is
> no different from any other template instantiation either.

That depends on what you're comparing to. If you're comparing to:

if (runtime_condition)
exec<A>();
else
exec<B>();

then I agree.

Nicol Bolas

unread,
May 23, 2017, 1:15:28 PM5/23/17
to ISO C++ Standard - Future Proposals

Well yes, you could implement the feature in a way that throws performance away. But I don't think we should evaluate a feature by assuming that compiler developers will implement it badly for no reason. The main point is that compilers are perfectly capable of implementing the operation in an optimal fashion.

Also, the result of contextually converting `runtime_condition` to `bool` needs to be stored, since it cannot be evaluated more than once.

ma.ka...@web.de

unread,
May 23, 2017, 2:20:17 PM5/23/17
to ISO C++ Standard - Future Proposals
Sorry. This was misleading. I know that we cannot avoid the exponential growth. My point is, that there is no accidental exponential growth, because the programmer has to explicitly denote the usage with the [[generic]] attribute.

Thiago Macieira

unread,
May 24, 2017, 12:50:32 PM5/24/17
to std-pr...@isocpp.org
On Tuesday, 23 May 2017 11:20:17 PDT ma.ka...@web.de wrote:
> Sorry. This was misleading. I know that we cannot avoid the exponential
> growth. My point is, that there is no accidental exponential growth,
> because the programmer has to explicitly denote the usage with the
> [[generic]] attribute.

You may notice I ignored your emails talking about the attribute because
that's an invalid use of an attribute. An attribute must be ignorable by a
compiler that doesn't know it and the program must still compile the same way
and run the same way.

You need a new keyword or a new operator.
Message has been deleted

ma.ka...@googlemail.com

unread,
May 24, 2017, 1:22:38 PM5/24/17
to ISO C++ Standard - Future Proposals
Thanks for the hint. When you replace all occurences of [[generic]] with generic, it should be ok.

//generic lambda
generic
[](bool runtime_condition) {
   
if(runtime_condition) {
       
return A{};
   
} else {
       
return B{};
   
}
}

// generic function
generic
auto doit(bool runtime_condition) {
   
if(runtime_condition) {
       
return A{};
   
} else {
       
return B{};
   
}    
}

// generic function with generic ternary operator
generic
auto doit(bool runtime_condition) {

   
return runtime_condition generic ? A{} : B{};

}



// generic variable
void doit(bool runtime_condition) {
    generic
auto x;

   
if(runtime_condition) {
        x
= A{};
   
} else {
        x
= B{};
   
}    
}

// generic variable with concept
void doit(bool runtime_condition) {
    generic
Iteratable x;

   
if(runtime_condition) {
        x
= A{};
   
} else {
        x
= B{};
   
}    
}



As an alternative generic could be a compile time variant. But this way, the ternary operator could not be marked as generic.


//generic lambda
[](bool runtime_condition)-> generic(auto) {

   
if(runtime_condition) {
       
return A{};
   
} else {
       
return B{};
   
}
}

// generic function
generic(auto) doit(bool runtime_condition) {

   
if(runtime_condition) {
       
return A{};
   
} else {
       
return B{};
   
}    
}

// generic function with generic ternary operator
generic(auto) doit(bool runtime_condition) {
   
return runtime_condition [[generic]] ? A{} : B{};

}



// generic variable
void doit(bool runtime_condition) {

    generic(
auto) x;


   
if(runtime_condition) {
        x
= A{};
   
} else {
        x
= B{};
   
}    
}

// generic variable with concept
void doit(bool runtime_condition) {

   
generic(Iteratable) x;


   
if(runtime_condition) {
        x
= A{};
   
} else {
        x
= B{};
   
}    
}


The second version could be extended with limited type sets. For example generic(int, float, double). Or with concepts: generic(Iteratable, Indexable) or something like that.
I'm unsure abour generic(auto). Maybe generic(...) or generic(any). or a simple generic.
Reply all
Reply to author
Forward
Message has been deleted
0 new messages