Opened 6 years ago

Last modified 2 weeks ago

#147 new defect

Can't initialize const member of struct

Reported by: mlbrooks Owned by:
Priority: major Component: cfa-cc
Version: 1.0 Keywords:
Cc:

Description (last modified by mlbrooks)

[A, basic]

#ifdef HIDE_ERR
    #define MAYBE_QUAL(...)
#else
    #define MAYBE_QUAL(...) __VA_ARGS__
#endif

struct thing {
    MAYBE_QUAL( const ) int foo; 
};
void ?{}( thing & this ) { 
    (this.foo){ 5 }; 
}
int main() {
    thing t;
    printf("%d\n", t.foo);
}

A, expected, regardless of defines: compiles, runs, and prints 5

A, actual, no define: error: lvalue required as unary ‘&’ operand

A, actual, -DHIDE_ERR: (as expected)

When the bug occurs, CFA-cc passes bad C code to GCC; this code tries to take address of address. This behaviour occurs in spite of #166 having been fixed, though perhaps the technique of #166 can be applied in a different place to fix this issue.

Excerpt of -CFA, no define

((void)(((void)(_tmp_ctor_expr0=(*((unsigned long int **)(&(&(*_X4thisS6thingy_1)._X3fooKm_1)))
                                )
               )
        )
       ,(((void)((*_tmp_ctor_expr0)=((unsigned long int )5)) /* ?{} */)
        ,_tmp_ctor_expr0
        )
       )
);

Excerpt -f -CFA, -DHIDE_ERR

((void)(((void)(_tmp_ctor_expr0=(                           &(*_X4thisS6thingy_1)._X3foom_1
                                )
               )
        )
       ,(((void)((*_tmp_ctor_expr0)=((unsigned long int )5)) /* ?{} */)
        ,_tmp_ctor_expr0
        )
       )
);

By contrast, initializing a const field via the autogenerated member-wise constructor works:

[B, success corner]

struct thing {
    const int foo; 
};
int main() {
    thing t{ 1234 };
    printf("%d\n", t.foo);
}

B, actual and expected: compiles, runs, and prints 1234

We found a workaround that uses both field assignment (in place of field constructor call) and a const-cast:

[C, workaround]

struct thing {
    const int foo; 
};
void ?{}( thing & this ) { 
    * ( int * ) & ( this.foo ) = 5;
}
int main() {
    thing t;
    printf("%d\n", t.foo);
}

C, actual and expected: compiles, runs, and prints 5

Further variations of the basic pattern have been tried, with mixed success:
[D, mix-n-match]

#ifndef VAR_noqual
    // default: include qualifiers
    #define MAYBE_QUAL(...) __VA_ARGS__
#else
    // variation: suppress qualifiers
    #define MAYBE_QUAL(...)
#endif

#ifndef VAR_assign
    // default: user's ctor calls field's ctor
    #define INITIALIZE( THISFIELD, VAL ) ( THISFIELD ) { VAL }
#else
    // variation: user's ctor calls field's assignment operator
    #define INITIALIZE( THISFIELD, VAL ) ( THISFIELD ) = ( VAL )
#endif

#if ! defined VAR_concast1  &&  ! defined VAR_concast2
    // default: user's field access is otherwise passthrough
    #define MAYBE_CONCAST( TY, THISFIELD ) THISFIELD
#elif defined VAR_concast1  &&  ! defined VAR_concast2
    // variation, sub 1: user's field access is via a CFA const cast
    #define MAYBE_CONCAST( TY, THISFIELD ) ( TY & )( THISFIELD )
#elif ! defined VAR_concast1  &&  defined VAR_concast2
    // variation, sub 1: user's field access is via a C const cast
    #define MAYBE_CONCAST( TY, THISFIELD ) * ( TY * )( & ( THISFIELD ) )
#else
    #error Bad 'concast' combination
#endif

#ifndef VAR_derefref
    // default: passthrough
    #define MAYBE_DEREFREF( THISFIELD ) THISFIELD
#else
    // variation: take the address, then dereference it
    // (has helped some cases of emitting "lvalue required" code)
    #define MAYBE_DEREFREF( THISFIELD ) *&(THISFIELD)
#endif

#ifndef VAR_ctorparm
    // default: user's ctor is parameterless; ctor's body hardcodes the value
    #define MAYBE_CTORPARM( ... )
    #define SWITCH_CTORPARM( CASE_NORMAL, CASE_VAR ) CASE_NORMAL
#else
    // variation: user's ctor takes a `foo_val` param; program main hardcodes the value
    #define MAYBE_CTORPARM( ... ) __VA_ARGS__
    #define SWITCH_CTORPARM( CASE_NORMAL, CASE_VAR ) CASE_VAR
#endif

#ifndef VAR_with
    // default: ctor body accesses the field via this-dot
    #define SWITCH_WITH( CASE_NORMAL, CASE_VAR ) CASE_NORMAL
#else
    // variation: ctor body accesses the field via `with`
    #define SWITCH_WITH( CASE_NORMAL, CASE_VAR ) CASE_VAR
#endif

struct thing {
    MAYBE_QUAL( const ) int foo; 
};
void ?{}( thing & this  MAYBE_CTORPARM(, int foo_val ) ) SWITCH_WITH( , with( this ) ) { 
    INITIALIZE( MAYBE_DEREFREF( MAYBE_CONCAST( int, SWITCH_WITH( this., ) foo ) ), SWITCH_CTORPARM( 42, foo_val ) );
}
int main() {
    thing t{ MAYBE_CTORPARM( 999 ) };
    printf("%d\n", t.foo);
}

D, all variation combinations, ideal: success, printing 5 or 999, as appropriate

D, expectation tolerance: as can be accepted case-by-case, rejection of some "ideal-valid" variation combinations may occur due to cfacc having to resort to conservative detection of "this appears to be a field initialization."

D, across variation combinations, actual: incomplete list follows

Some of the earlier tests are variation combinations from this general test, specifically:

  • A is D with A's HIDE_ERROR being the same as D's VAR_noqual, and with the other VAR_* not defined [actual, VAR_noqual, pass; actual, none defined, fail]
  • C is D with VAR_assign and VAR_concast2 defined, and the other VAR_* not defined [actual: pass]

However, in spite of the element of similarity, B is not D with VAR_ctorparm defined and the other VAR_* not defined. B (pass) uses an autogenerated constructor, while D with VAR_ctorparm defined (fail) uses a user-written constructor that tries to emulate B.

Every VAR_* option is believed to be at least a benign independent contribution, i.e. not inherently invalid. This is directly observable in all cases VAR_xxx, except VAR_concast2, defining both VAR_noqual (pass on its own) and VAR_xxx leaves a working program (VAR_noqual + VAR_xxx: pass). For VAR_concast2, its validity along with VAR_assign (as in test C) shows that it can be okay.

Even so, VAR_concast2 all alone is failing, with the same symptom as in A. No hypothesis generalizing this phenomenon is yet conceived.

VAR_assign alone: different error, from CFA resolver (before chance to generate bad code): ambiguous ?{} call, between regular and volatile; not remotely understood; have been unable to fix, with any (non)volatile combinations (these not shown)

VAR_assign + VAR_concast1: fixed, as for VAR_concast2 already discussed

VAR_derefref: different error still, also at resolver: ctor call is an invalid application

Except for VAR_noqual, each other VAR_*, on its own has some error. Except as just identified (VAR_assign and VAR_derefref), the symptom is "fails to fix the original symptom."

The switches VAR_ctorparm and VAR_with have never been seen to have an effect, i.e. these factors are independent.

Note that the -E and -CFA -XCFA,-p outputs from program D are quite readable. Looking at a -E can be easier than mentally expanding macros.

Change History (1)

comment:1 by mlbrooks, 2 weeks ago

Description: modified (diff)
Priority: minormajor

Elaborated description with recent testing outcomes and a newly found workaround. Increased priority because this issue was encountered during CS-343 assignment porting.

Note: See TracTickets for help on using tickets.