What's the difference between conditional compilation and the conditional attribute?

What's the difference between conditional compilation and the conditional attribute?

Rate This
  • Comments 10

User: Why does this program not compile correctly in the release build?

class Program
{
#if DEBUG
    static int testCounter = 0;
#endif 
    static void Main(string[] args)
    {
        SomeTestMethod(testCounter++);
    } 
    [Conditional("DEBUG")]
    static void SomeTestMethod(int t) { }
}

Eric: This fails to compile in the retail build because testCounter cannot be found in the call to SomeTestMethod.

User: But that call site is going to be omitted anyway, so why does it matter? Clearly there's some difference here between removing code with the conditional compilation directive versus using the conditional attribute, but what's the difference?

Eric: You already know the answer to your question, you just don't know it yet. Let's get Socratic; let me turn this around and ask you how this works. How does the compiler know to remove the method call site? 

User: Because the method called has the conditional attribute on it.

Eric: You know that. But how does the compiler know that the method called has the conditional attribute on it?

User: Because overload resolution chose that method. If this were a method from an assembly, the metadata associated with that method has the attribute. If it is a method in source code, the compiler knows that the attribute is there because the compiler can analyze the source code and figure out the meaning of the attribute.

Eric: I see. So fundamentally, overload resolution does the heavy lifting. How does overload resolution know to choose that method? Suppose hypothetically there were another method of the same name with different parameters.

User: Overload resolution works by examining the arguments to the call and comparing them to the parameter types of each candidate method and then choosing the unique best match of all the candidates.

Eric: And there you go. Therefore the arguments must be well-defined at the point of the call, even if the call is going to be removed. In fact, the call cannot be removed unless the arguments are extant! But in the release build, the type of the argument cannot be determined because its declaration has been removed.

So now you see that the real difference between these two techniques for removing unwanted code is what the compiler is doing when the removal happens. At a high level, the compiler processes a text file like this. First it "lexes" the file. That is, it breaks the string down into "tokens" -- sequences of letters, numbers and symbols that are meaningful to the compiler. Then those tokens are "parsed" to make sure that the program conforms to the grammar of C#. Then the parsed state is analyzed to determine semantic information about it; what all the types are of all the expressions and so on. And finally, the compiler spits out code that implements those semantics.

The effect of a conditional compilation directive happens at lex time; anything that is inside a removed #if block is treated by the lexer as a comment. It's like you simply deleted the whole contents of the block and replaced it with whitespace. But removal of call sites depending on conditional attributes happens at semantic analysis time; everything necessary to perform that semantic analysis must be present. 

User: Fascinating. Which parts of the C# specification define this behavior?

Eric: The specification begins with a handy “table of contents”, which is very useful for answering such questions. The table of contents states that section 2.5.1 describes "Conditional compilation symbols" and section 17.4.2 describes "The Conditional attribute".

User: Awesome.

  • Great post.

    There's probably a typo in your first answer. Should be release build instead of retail build.

  • I always though it would have been nicer if the methods marked with [Conditional] attribute would have been removed by the JIT instead of the compiler, that way you could switch a retail assembly to debug mode and have all the extra debug information available like the debug output, without having a second debug version of the same assembly.

    What I would also like to ask is, are there any planned posts regarding the Reactive Framework and the new IObservable<T> and IObserver<T> interfaces, or those are just planned at library level so far?

    Like if IObservable<T> and IObserver<T> are the counterparts of IEnumerable<T> and IEnumerator<T> what would the counterpart of yield be?

  • @Cata: I think you're thinking too low level with 'yield'. The high level construct would be query comprehension expressions (select, where, etc.), which should be supported "normally" with the Reactive framework. Thus, no new keyword is necessary, as in-language LINQ support will be sufficient.

  • ...though it should be noted that low level support such as a 'push return' keyword would make *implementing* the Reactive framework itself easier, just as 'yield' greatly simplifies implementing LINQ extension methods...

    That said, if C# had LINQ before 'yield,' I'm not sure that 'yield' would have been added to the language.  See VB.NET...

  • What can get "real interesting" is code like the following (illustrative, not actual code...)

    [Conditional]

    void Foo(DervivedClass v)  {}

    void Fool(BaseClass v) {}

    class BaseClass {}

    class DerivedClass : BaseClass {}

    void Bar()

    {

      DerivedClass d;

      Foo(d);    / Who gets called......

    }

  • @TheCPUWizard, no one get's called, it clearly stated that calls to methods with conditional attribute are removed, which means the call is removed.

    However, I've also tested this, and the call is removed as expected opposed to being re-evaluated, which the spec doesn't say it should happen.

  • @Cata - You are exactly correct. But many people get this wrong. I know of at least one case where this mistake lead directly to a very subtle bug [the conditional method was added for "debugging/isolation" of one specific derived class and internally called the "normal" inplementation. Before this was done all classes in the derivation tree would get processed by the base method. In release builds after this change, the one specific derived class did not get processed AT ALL!]

    I haven't had the chance to test the following, but I wonder how the 4.0 behaviour would be if "d" was declared as dynamic...............Seems like something I will try over the weekend.

  • TheCPUWizard: Jesus, these typesystem-feature interferences increases the necessary knowledege of programming in C# exponentially. This makes me sad. If you need pilot licence for developing in a language, that language missed the point.

  • I bet on something: that user does not exist in real life.

    First, his narrow answer to your first question serves too well the rest of the question/answer path.  Why did he choose "overload resolution" before being asked a more specific question?

    Second, his second answer, again right on target ("Overload resolution works by examining the arguments") proves he's not a beginner and thus, it should already have rang the bells in this head.  It's clear that "examining the arguments" is the key to "why does it look at the argument"...

    Am I wrong? :-P

    The user exists but of course the dialogue is mostly made up. The didactic dialogue has a long history; Plato's dialogues were certainly not word-for-word transcriptions of Socrates' conversations either. The purpose of the dialogue structure is to provide a framework upon which to hang the instructive text, not to be an accurate account of a real conversation. Try recording a word-for-word transcription of a technical conversation over lunch sometime and you'll see why; it's completely incoherent on the page. -- Eric

     

  • Haha!!  You're so right!!  :-)  I didn't mean no disrespect though, I just felt like playing Sherlock a bit today.  In all cases, I love your blog and the way you bring that up.

Page 1 of 1 (10 items)