0
votes

I'm reading the Standard: N1570 and came across some misunderstanding. I wrote the following simple example:

test.h:

#ifndef TEST_H
#define TEST_H

extern int second;

#endif //TEST_H

test.c:

#include "test.h"

enum test_enum{ 
    first,
    second
};

But it fails to compile with the error:

error: ‘second’ redeclared as different kind of symbol
     second
     ^~~~~~

This is strange, because Section 6.4.4.3#2 specifies:

2 An identifier declared as an enumeration constant has type int.

I our case the enumeration constant has file scope so I expected it to compile fine.

I rewrote the example above as follows: main.c:

extern int second;
int main(int argc, char const *argv[])
{
    printf("Second: %d\n", second);
}

And now linker complains:

undefined reference to `second'

Why? It should find the definition in the test.c because as the Section 6.2.2#5 specifies:

If the declaration of an identifier for an object has file scope and no storage-class specifier, its linkage is external.

2
You may also reference the web page holding the C11 Standard - Draft n1570 to prevent having everyone download the entire .pdf each time (both are fine, just a suggestion) - David C. Rankin
You are including test.h which contains extern int second; When you declare an enum, the label (enum members) are also global constants -- the compiler sees two declarations of second -- thus the redeclaration. - David C. Rankin
@DavidC.Rankin The problem is if I replace the declaration of enumeration constant with int it works as expected. - St.Antario
Either move the enum to the header and include test.h in main, or rename the variable second to something else in test.h and define it in test.c. - David C. Rankin
The first snippet would require the notion of different namespaces for different types so the identifier is not ambiguous. C only has that for struct members. Early K&R didn't, very painful. Notable that C++ fixed that just recently with enum class. The second snippet has extern ("it lives somewhere else") in all the code, the linker correctly complains "where?" Delete extern in the main.c file to say "here". - Hans Passant

2 Answers

2
votes

Objects and Constants Are Different Things

Your citation of 6.4.4.3 2, that an enumeration constant has type int, suggests you think that because extern int second and enum { second } declare second to be an int, that these two declarations of second may refer to the same thing. That is not correct.

extern int second declares second to be the name of an object (a region of memory) that will hold an int. enum { second } declares second to be an enumeration constant which will have a particular value. The enumeration constant has no object (no memory) associated with it. An int object and an int constant are different things, and you may not use the same identifier for them in the same scope.

Not All Declarations Are Definitions

Regarding your question about the link error, “undefined reference to ‘second’”, although test.c may contain external int second (since it is provided by the included test.h, this is not a definition of second. It is only a declaration, which tells the compiler that the name refers to an object. It does not define the object. Alternatively, if test.c contains enum { second }, this only declares second to be a constant. It does not define an object.

The rules for what is a definition are a bit complicated due to the history of programming language development. For identifiers of objects declared at file scope, there are essentially four cases:

  • A declaration with extern is only a declaration, not a definition. Example: extern int second;.
  • A declaration with an initializer is a definition. Example: int second = 2;.
  • A declaration without extern and without an initializer is a tentative definition. If no definition appears in the translation unit (source file being compiled, with all included files), the tentative definition becomes a definition. Example: int second;.

The linkage is not helpful here. The extern int second in test.c and the extern int second in main.c may refer to the same object due to linkage, but no object for them to refer to was defined. Or, in the alternative, if test.c contains enum { second }, then it does not define an object named second, so there is no object that the extern int second in main.c can refer to.

2
votes

To do what you are attempting, you simply need to refactor you code a bit so that test.[ch] does not see a redeclaration of second. The problem is the symbol second is defined once as extern int second; and then again as the symbol within the enum. You can't have both visible within the same file.

To do that you can write test1.h using a second preprocessor conditional similar to:

#ifndef TEST_H
#define TEST_H

#ifdef USE_ENUM
    enum test_enum{
        first,
        second
    };
#else
    extern int second;
#endif

#endif

Where depending on whether USE_ENUM is defined, the code will use either the symbol provided by the enum, where if not, then you need to define second in test1.c

#include "test1.h"

#ifdef USE_ENUM
    char stub (void)    /* stub to prevent empty compilation unit */
    { return 0; }
#else
    int second = 2;
#endif

(note the use of a stub function to prevent an empty compilation unit if USE_ENUM is defined -- as there would be no code in test1.c otherwise)

Now all that is required is to include test1.h in your file containing main() and passing the compiler define -DUSE_ENUM as a compiler option depending on which code you want to use, e.g.

#include <stdio.h>
#include "test1.h"

int main (void) {

    printf ("second: %d\n", second);

}

Compile Using int second defined in test.c

Example:

$ gcc -Wall -Wextra -pedantic -std=c11 -o bin/main1 main1.c test1.c

Example Use/Output

When USE_ENUM is not defined, then the definition of second defined in test1.c and accessed via extern will result in second having the value 2, e.g.

$ ./bin/main1
second: 2

Compile Using enum defined in test.h

Example:

$ gcc -Wall -Wextra -pedantic -std=c11 -o bin/main1 main1.c test1.c -DUSE_ENUM

Example Use/Output

When USE_ENUM is defined, then the value for the symbol second is provided by the enum in test1.h, e.g.

$ ./bin/main1
second: 1

While this is a slight refactoring of what you were attempting, I don't see another way of doing both without using the preprocessor conditional.