To what extent is C++ a statically-typed language?
Asked Answered
B

3

22

I used to think that the answer to this question was "100%", but I've recently been pointed to an example that makes it worth thinking twice. Consider a C array declared as an object with automatic storage duration:

int main()
{
    int foo[42] = { 0 };
}

Here, the type of foo is clearly int[42]. Consider, instead, this case:

int main()
{
    int* foo = new int[rand() % 42];
    delete[] foo;
}

Here, the type of foo is int*, but how can one tell the type of the object created by the new expression at compile-time? (Emphasis is meant to stress the fact that I am not talking about the pointer returned by the new expression, but rather about the array object created by the new expression).

This is what Paragraph 5.3.4/1 of the C++11 Standard specifies about the result of a new expression:

[...] Entities created by a new-expression have dynamic storage duration (3.7.4). [ Note: the lifetime of such an entity is not necessarily restricted to the scope in which it is created. —end note ] If the entity is a non-array object, the new-expression returns a pointer to the object created. If it is an array, the new-expression returns a pointer to the initial element of the array.

I used to think that in C++ the type of all objects is determined at compile-time, but the above example seems to disprove that belief. Also, per Paragraph 1.8/1:

[...] The properties of an object are determined when the object is created. An object can have a name (Clause 3). An object has a storage duration (3.7) which influences its lifetime (3.8). An object has a type (3.9). [...]

So my questions are:

  1. What is meant by "properties" in the last quoted paragraph? Clearly, the name of an object cannot count as something which is determined "when the object is created"- unless "created" here means something different than I think;
  2. Are there other examples of objects whose type is determined only at run-time?
  3. To what extent is it correct to say that C++ is a statically-typed language? Or rather, what is the most proper way of classifying C++ in this respect?

It would be great if anybody could elaborate at least on one of the above points.

EDIT:

The Standard seems to make it clear that the new expression does indeed create an array object, and not just several objects laid out as an array as pointed out by some. Per Paragraph 5.3.4/5 (courtesy of Xeo):

When the allocated object is an array (that is, the noptr-new-declarator syntax is used or the new-type-id or type-id denotes an array type), the new-expression yields a pointer to the initial element (if any) of the array. [ Note: both new int and new int[10] have type int* and the type of new int[i][10] is int (*)[10] —end note ] The attribute-specifier-seq in a noptr-new-declarator appertains to the associated array type.

Bugg answered 10/4, 2013 at 23:7 Comment(11)
I think you're confusing dynamic and a static. Your example will return an int*.. regardless of how many indices rand() returns. That's dynamic memory allocation.. not dynamic typing.Yokum
I'd almost call it "Type erasure"Harhay
For correctionness sake, shouldn't you delete[] the pointer?Aguirre
@Spidey: Shame on me for that. Of course. Edited, thank youBugg
This reminds me of ISO Pascal, where arrays of different sizes were strictly different types. That made e.g. string processing virtually impossible.Showbread
Using C-style arrays to probe the C++ type system leads to insanity. They have rather quirky properties, inherited from C, that don't fit well into a rigorous system of types. So the answer is "so what?".Filigree
@PeteBecker: Mine was mostly a theoretical interest. Notice, that the question asks whether there are other such examples of objects whose type is determined only at run-time. Your "so what?" seems to imply that 1) it is indeed the case that the type is determined only at run-time, and 2) there are no other similar cases. Then you could elaborate a bit and make an answer out of it. Also, the point in question 1 remains unclear to me: if type is a "property" of an object, and C++ is statically typed, I wouldn't expect properties to be determined "when the object is created" (1.8/1).Bugg
String literals (prior to C++11) are also quirky, in addition to the quirks they get from being arrays. And character literals have a few quirks, too. For understanding the type system, they're distractions, not cleanly integrated parts. Which is why I said "so what"; I find them highly uninteresting. They are what they are, and they don't fit well into a rational system.Filigree
@PeteBecker: OK, I see your point. But it is still not clear to me what is meant in 1.8/1 by "properties", nor by "when the object is created" (according to my intuition, creation happens at run-time)Bugg
You know what? This is a great candidate to enter Stroustrup's official C++ faq.Aguirre
I think that that sentence is descriptive, not normative. That is, it doesn't impose any requirements; the rules for construction and destruction tell you what an object is and what its "properties" are. But that's just a superficial reading, so don't take it to heart.Filigree
P
9

The terms 'static type' and 'dynamic type' apply to expressions.

static type

type of an expression (3.9) resulting from analysis of the program without considering execution semantics


dynamic type

<glvalue> type of the most derived object (1.8) to which the glvalue denoted by a glvalue expression refers

Additionally, you can see that a dynamic type only differs from a static type when the static type can be derived from, which means a dynamic array type is always the same as the expression's static type.

So your question:

but how can one tell the type of the object created by the new expression at compile-time?

Objects have types, but they're not 'static' or 'dynamic' types absent an expression that refers to the object. Given an expression, the static type is always known at compile time. In the absence of derivation the dynamic type is the same as the static type.

But you're asking about objects' types independent of expressions. In the example you give you've asked for an object to be created but you don't specify the type of object you want to have created at compile time. You can look at it like this:

template<typename T>
T *create_array(size_t s) {
    switch(s) {
        case 1: return &(*new std::array<T, 1>)[0];
        case 2: return &(*new std::array<T, 2>)[0];
        // ...
    }
}

There's little special or unique about this. Another possibility is:

struct B { virtual ~B() {}};
struct D : B {};
struct E : B {};

B *create() {
    if (std::bernoulli_distribution(0.5)(std::default_random_engine())) {
        return new D;
    }
    return new E;
}

Or:

void *create() {
    if (std::bernoulli_distribution(0.5)(std::default_random_engine())) {
        return reinterpret_cast<void*>(new int);
    }
    return reinterpret_cast<void*>(new float);
}

The only difference with new int[] is that you can't see into its implementation to see it selecting between different types of objects to create.

Papain answered 11/4, 2013 at 18:27 Comment(2)
Agreed. In addition, Wikipedia defines "static type system" as "type checking is performed during compile-time as opposed to run-time.", and "dynamic type system" as "the majority of its type checking is performed at run-time as opposed to at compile-time". As C++ does no type checks at runtime, this is all still static. (Unrelated, C++ is weakly typed, but safely typed)Harhay
@MooingDuck: C++ actually can do some limited run-time type checking, in the form of dynamic_cast. If the class has a vtable, and the type can't be statically guaranteed, dynamic_cast can and does do runtime checks to make sure the thing you're pointing at is, or inherits from, the type you're trying to cast it to.Wharton
H
10

The new-expression doesn't create an object with runtime-varying array type. It creates many objects, each of static type int. The number of these objects is not known statically.


C++ provides two cases (section 5.2.8) for dynamic type:

  • Same as the static type of the expression
  • When the static type is polymorphic, the runtime type of the most-derived object

Neither of these gives any object created by new int[N] a dynamic array type.


Pedantically, evaluation of the new-expression creates an infinite number of overlapping array objects. From 3.8p2:

[ Note: The lifetime of an array object starts as soon as storage with proper size and alignment is obtained, and its lifetime ends when the storage which the array occupies is reused or released. 12.6.2 describes the lifetime of base and member subobjects. — end note ]

So if you want to talk about the "array object" created by new int[5], you have to give it not only type int[5] but also int[4], int[1], char[5*sizeof(int)], and struct s { int x; }[5].

I submit that this is equivalent to saying that array types do not exist at runtime. The type of an object is supposed to be restrictive, information, and tell you something about its properties. Allowing a memory area to be treated as an infinite number of overlapping array objects with different type in effect means that the array object is completely typeless. The notion of runtime type only makes sense for the element objects stored within the array.

Hankering answered 10/4, 2013 at 23:11 Comment(21)
But the array itself is an object and has a type which includes its size, no? From the standard: "If it is an array, the new-expression returns a pointer to the initial element of the array."Secund
@sftrabbit: No. You cannot query for the size after the thing is created. It isn't a separate "array" type; it is just a bunch of ints adjacent to each other.Ferriter
@BillyONeal But that doesn't mean the array isn't there... okay, this quickly became a philosophical debate.Secund
@sftrabbit: There is nothing here that has array type. There are objects arrayed in memory, but I'm not even sure this arrangement constitutes an "array object" that you could safely bind to a reference-to-array variable, even with casting.Hankering
@BenVoigt I think the problem is that you simply can't get access to this array object in any meaningful way. You can only ever have a pointer to its first element. There's no way to recover the array type from the pointer type. So yes, I'm agreeing that it doesn't really even make sense to talk about the type of the array.Secund
@sftrabbit: If the array even is an object in the first place.Hankering
@BenVoigt Well here's how the standard phrases it: "Entities created by a new-expression have dynamic storage duration. If the entity is a nonarray object, [...]. If it is an array, [...]" Sounds like it's an object to me.Secund
All this basically comes from an example that was given to me by Johannes Schaub.Bugg
@sftrabbit: Yes, clearly the non-array is an object. The Standard isn't telling us whether the array case is an array object or just a tightly packed group of individual objects.Hankering
@sftrabbit: Yes, it does mean it isn't there. A type that cannot be accessed in any meaningful way is not a type. That is, if you did decltype(new int[rand()/2]) you will get int*, not int[42].Ferriter
@BillyONeal I agree. That's the real point here. The type is completely meaningless. It cannot in any way affect the program.Secund
§5.3.4/5: "When the allocated object is an array (that is, the noptr-new-declarator syntax is used or the new-type-id or type-id denotes an array type), the new-expression yields a pointer to the initial element (if any) of the array. [...] The attribute-specifier-seq in a noptr-new-declarator appertains to the associated array type." -- The standard clearly says that an array object is being created, so this answer doesn't seem right.Damned
@Xeo: Way to quote only part of the paragraph... you left out the part that said the created object does not have array type.Hankering
@Ben: You mean the note? That says that the expressions new int and new int[10] have type int*. That isn't relevant for the type of the created object.Damned
@Xeo: Anyway, it's pretty clear that a dynamic type can never be an array type. The address of the array is also the address of its first element, and that has a type known at compile time.Hankering
I question the use of the Standard term 'dynamic type' in your answer and your comments -- you seem to be misusing it. E.g. an object of array type is a most derived object, hence it has dynamic type that array type. So there is such a thing as a dynamic array type.Tranquilizer
For the moment, I unaccepted the answer because what seemed to me like a plausible interpretation of what new does (create several objects, but not one array object) is disproved by the paragraph @Damned quoted in a comment, as far as I can tell.Bugg
@Luc: AFAICT, the dynamic type of an array object is the runtime type of its first element (which shares the same address). Array objects are not polymorphic, and thus never have dynamic type different from the static type.Hankering
So you see how they have a dynamic type then. All that remains is to fix your answer.Tranquilizer
@LucDanton: I clarified my answer. There's no object here with a runtime-varying array type. The question was whether any types are generated at runtime or they all exist at compile-time, and there's no type here generated at runtime. (You can have dynamic objects with array type, but it is still a static array type, for example with p = new int[N][4]; the type of *p is an array type, but there no runtime variation in it.Hankering
My objection is to your co-opting of the Standard term 'dynamic type', which has a very precise meaning that is irrelevant here. 'Runtime-varying array type' does not have such baggage.Tranquilizer
P
9

The terms 'static type' and 'dynamic type' apply to expressions.

static type

type of an expression (3.9) resulting from analysis of the program without considering execution semantics


dynamic type

<glvalue> type of the most derived object (1.8) to which the glvalue denoted by a glvalue expression refers

Additionally, you can see that a dynamic type only differs from a static type when the static type can be derived from, which means a dynamic array type is always the same as the expression's static type.

So your question:

but how can one tell the type of the object created by the new expression at compile-time?

Objects have types, but they're not 'static' or 'dynamic' types absent an expression that refers to the object. Given an expression, the static type is always known at compile time. In the absence of derivation the dynamic type is the same as the static type.

But you're asking about objects' types independent of expressions. In the example you give you've asked for an object to be created but you don't specify the type of object you want to have created at compile time. You can look at it like this:

template<typename T>
T *create_array(size_t s) {
    switch(s) {
        case 1: return &(*new std::array<T, 1>)[0];
        case 2: return &(*new std::array<T, 2>)[0];
        // ...
    }
}

There's little special or unique about this. Another possibility is:

struct B { virtual ~B() {}};
struct D : B {};
struct E : B {};

B *create() {
    if (std::bernoulli_distribution(0.5)(std::default_random_engine())) {
        return new D;
    }
    return new E;
}

Or:

void *create() {
    if (std::bernoulli_distribution(0.5)(std::default_random_engine())) {
        return reinterpret_cast<void*>(new int);
    }
    return reinterpret_cast<void*>(new float);
}

The only difference with new int[] is that you can't see into its implementation to see it selecting between different types of objects to create.

Papain answered 11/4, 2013 at 18:27 Comment(2)
Agreed. In addition, Wikipedia defines "static type system" as "type checking is performed during compile-time as opposed to run-time.", and "dynamic type system" as "the majority of its type checking is performed at run-time as opposed to at compile-time". As C++ does no type checks at runtime, this is all still static. (Unrelated, C++ is weakly typed, but safely typed)Harhay
@MooingDuck: C++ actually can do some limited run-time type checking, in the form of dynamic_cast. If the class has a vtable, and the type can't be statically guaranteed, dynamic_cast can and does do runtime checks to make sure the thing you're pointing at is, or inherits from, the type you're trying to cast it to.Wharton
F
2

I used to think that in C++ the type of all objects is determined at compile-time, but the above example seems to disprove that belief.

The example you cite is talking about storage duration of the item. C++ recognizes three storage durations:

  1. Static storage duration is the duration of global and local static variables.
  2. Automatic storage duration is the duration for "stack allocated" function-local variables.
  3. Dynamic storage duration is the duration for dynamically allocated memory such as that with new or malloc.

The use of the word "dynamic" here has nothing to do with the object's type. It refers to how an implementation must store the data that makes up an object.

I used to think that in C++ the type of all objects is determined at compile-time, but the above example seems to disprove that belief.

In your example, there is one variable, which has type int*. There is not an actual array type for the underlying array which can be recovered in any meaningful way to the program. There is no dynamic typing going on.

Ferriter answered 10/4, 2013 at 23:13 Comment(7)
I know that "ipse dixit" is not exactly the best counter-argument, but here is the comment by Johannes Schaub that made me think about this. I am aware of the three storage durations and about the fact that new returns an int*. What puzzles me is whether there is a "created object" which has array type but where the size of the array (and therefore the type) is only known at run-timeBugg
@R.MartinhoFernandes: Sorry, I still mostly think in C++03 land :)Ferriter
@Andy: There is no concept of a type at runtime in C++.Ferriter
@Billy: Of course there is: typeid is one of the major "Runtime Type Information" tools (RTTI). But the types always existed at compile time also, the only thing that changes at runtime is how many objects exist of each type and what pointers (and references) are associated with what objects.Hankering
@Billy: That's what I believed (in the sense that type is a property assigned statically to objects). If that comment had not come from Johannes, I would have easily disregarded it.Bugg
@Ben: Of course, typeid still would not give you int[allocated size].Ferriter
@BillyONeal: Right -- arrays play no part in polymorphism.Hankering

© 2022 - 2024 — McMap. All rights reserved.