您的位置:首页 > 移动开发

Appearing and Disappearing consts in C++

2011-05-17 17:28 302 查看
If you write “
int i;
” in C++,
i
’s type seems obvious:
int
. If you write “
const int i;
”,
i
’s type seems equally obviously to be
const int
.
Often, these types are exactly what they seem to be, but sometimes
they’re not. Under certain circumstances, C++ treats non-const types as
const, and under others it treats const types as non-const.
Understanding when consts appear and disappear lends insight into the
behavior of C++, and that makes it possible to use the language more
effectively.

This article examines various aspects of type declaration and
deduction in both current standard C++ as well as the forthcoming
revised standard (C++0x), with an eye towards helping you understand how
and why the effective type of a variable can be different from what’s
“obvious.” You’ll find the article most useful if you are familiar with
the basic rules for template argument deduction, are aware of the
difference between lvalues and rvalues, and have had some exposure to
the new C++0x features lvalue and rvalue references,
auto
variables, and lambda expressions.

Given

int i;


what is the type of
i
? No, this is not a trick question, the type is
int
. But suppose we put
i
into a struct (or a class – it makes no difference):

struct S {
int i;
};


What is the type of
S::i
? Still
int
, right? Right. It’s not quite the same kind of
int
as the one outside the struct, because taking the address of
S::i
gives you a member pointer (i.e., an
int S::*
) instead of an
int*
, but
S::i
’s type is still
int
.

Now suppose we have a pointer to this struct:

S *ps;


What is the type of
ps->i
? (The fact that
ps
doesn’t point to anything in this example is not relevant. The expression
ps->i
has a type, even if executing the code would yield undefined behavior, e.g., a crash.)

This is still not a trick question. The type of
ps->i
is
int
. But what if we have a pointer to const?

const S* pcs;


What is the type of
pcs->i
? Now things are murkier.
pcs->i
refers to the int
i
in
S
, and we just agreed that the type of
S::i
is
int
. But when
S::i
is accessed through
pcs
,
S
becomes a const struct, and
S::i
is thus const. So one can argue that the type of
pcs->i
should be
const int
, not
int
.

In current C++ (i.e., the language defined by the international
standard adopted in 1998 and slightly revised in 2003, henceforth known
as C++98), the type of
pcs->i
is
const int
, and that’s pretty much the end of the story. In “new” C++ (i.e., the language defined by the international standard poised to be adopted this year and commonly referred to as C++0x), the type of
pcs->i
is also
const int
, which, given the importance of backwards compatibility, is reassuring. But the story is more nuanced.

decltype

C++0x introduces
decltype
, which is a way to refer to the type of an expression during compilation. Given the earlier declaration for
i
, for example,
decltype(i)
is
int
. But what happens when we apply
decltype
to
pcs->i
? The standardization committee could have simply decreed that it’s
const int
and been done with it, but the logic that says the type is
int
(i.e., the type of
S::i
, because, after all, we’re still referring to
i
in
S
)
isn’t unreasonable, and it turns out that being able to query the
declared type of a variable, independent of the expression used to
access it, is useful. Besides, this is C++, where, given a choice
between reasonable alternatives, the language often sits back and lets
you choose. That’s the case here.

The way you choose is quintessentially C++. If you apply
decltype
to the name of a variable, you get the declared type of that variable. For example, this declares variables
x
and
y
of the same types as
i
and
S::i
:

decltype(i) x;          // x is same type as i, i.e., int
decltype(pcs->i) y;     // y is same type as S::i, i.e., int


On the other hand, if you apply
decltype
to an
expression that is not the name of a variable, you get the effective
type of that expression. If the expression corresponds to an lvalue,
decltype
always returns an lvalue reference. I’ll show an example in a minute, but first I have to mention that while “
i
” is the name of a variable, “
(i)

is an expression that is not the name of a variable. In other words,
tossing parentheses around a variable yields an expression that has the
same runtime value as the variable, but during compilation, it’s just an
expression – not the name of a variable. This is important, because it
means that if you want to know the type of
S::i
when accessed through
pcs
,
decltype
will give it to you if you ask for it with parentheses around
pcs->i
:

decltype((pcs->i))      // type returned by decltype is const int& (see also
// discussion below)


Contrast this with the example above without the extra parentheses:

decltype(pcs->i)        // type returned by decltype is int


As I mentioned, when you ask
decltype
for the type of an
expression that is not the name of a variable, it returns an lvalue
reference if the type is an lvalue. That’s why
decltype((pcs->i))
returns
const int&
: because
pcs->i
is an lvalue. If you had a function that returned an
int
(const or non-const), that return type would be an rvalue, and
decltype
would indicate that by not reference-qualifying the type it returns for a call to that function:

const int f();          // function returning an rvalue

decltype(f()) x = 0;    // type returned by decltype for a call to f is
// const int (not const int&), so this declares x
// of type const int and initializes it to 0


Unless you’re a heavy-duty library writer, you probably won’t need to worry about the subtle behavioral details of
decltype
.
However, it’s certainly worth knowing that there may be a difference
between a variable’s declared type and the type of an expression used to
access that variable, and it’s equally worth knowing that
decltype
can help you distinguish them.

Type Deduction

You might think that this distinction between a variable’s declared
type and the type of an expression used to access it is new to C++0x,
but it fundamentally exists in C++98, too. Consider what happens during
type deduction for function templates:

template<typename T>
void f(T p);

int i;
const int ci = 0;
const int *pci = &i;

f(i);           // calls f<int>, i.e., T is int
f(ci);          // also calls f<int>, i.e., T is int
f(*pci);        // calls f<int> once again, i.e., T is still int


Top-level consts are stripped off during template type deduction1, so the type parameter
T
is deduced to be
int
for both
int
s and
const int
s,
both for variables and expressions, and this is true for both C++98 and
C++0x. That is, when you pass an expression of a particular type to a
template, the type seen (i.e., deduced) by the template may be different
from the type of the expression.

Type deduction for
auto
variables in C++0x is
essentially the same as for template parameters. (As far as I know, the
only difference between the two is that the type of
auto

variables may be deduced from initializer lists, while the types of
template parameters may not be.) Each of the following declarations
therefore declare variables of type
int
(never
const int
):

auto a1 = i;
auto a2 = ci;
auto a3 = *pci;
auto a4 = pcs->i;


During type deduction for template parameters and
auto

variables, only top-level consts are removed. Given a function template
taking a pointer or reference parameter, the constness of whatever is
pointed or referred to is retained:

template<typename T>
void f(T& p);

int i;
const int ci = 0;
const int *pci = &i;

f(i);               // as before, calls f<int>, i.e., T is int
f(ci);              // now calls f<const int>, i.e., T is const int
f(*pci);            // also calls f<const int>, i.e., T is const int


This behavior is old news, applying as it does to both C++98 and C++0x. The corresponding behavior for
auto
variables is, of course, new to C++0x:

auto& a1 = i;       // a1 is of type int&
auto& a2 = ci;      // a2 is of type const int&
auto& a3 = *pci;    // a3 is also of type const int&
auto& a4 = pcs->i;  // a4 is of type const int&, too


Lambda Expressions

Among the snazzier new features of C++0x is lambda expressions.
Lambda expressions generate function objects. When generated from
lambdas, such objects are known as closures, and the classes from which such objects are generated are known as closure types.
When a local variable is captured by value in a lambda, the closure
type generated for that lambda contains a data member corresponding to
the local variable, and the data member has the type of the variable.

If you’re not familiar with C++0x lambdas, what I just wrote is
gobbledygook, I know, but hold on, I’ll do my best to clear things up.
What I want you to notice at this point is that I said that the type of a
data member in a class has the same type as a local variable. That
means we have to know what “the same type as a local variable” means. If
it were drop-dead simple, I wouldn’t be writing about it.

But first let me try to summarize the relationship among lambdas,
closures, and closure types. Consider this function, where I’ve
highlighted the lambda expression:

void f(const std::vector<int>& v)
{
int offset = 4;
std::for_each(v.cbegin(), v.cend(),
[=](int i) { std::cout << i + offset; });
}


The lambda causes the compiler to generate a class that looks something like this:

class SomeMagicNameYouNeedNotKnow {
public:
SomeMagicNameYouNeedNotKnow(int p_offset): m_offset(p_offset) {}
void operator()(int i) const {  std::cout << i + m_offset; }
private:
int m_offset;
};


This class is the closure type. Inside
f
, the call to
std::for_each
is treated as if an object of this type – the closure – had been used in place of the lambda expression. That is, as if the call to
std::for_each
had been written this way:

std::for_each(v.cbegin(), v.cend(), SomeMagicNameYouNeedNotKnow(offset));


Our interest here is in the relationship between the local variable
offset
and the corresponding data member in the closure type, which I’ve called
m_offset
. (Compilers can use whatever names they like for the data members in a closure type, so don’t read anything into my use of
m_offset
.) Given that
offset
is of type
int
, it’s not surprising that
m_offset
’s type is also
int
, but notice that, in accord with the rules for generating closure types from lambdas,
operator()
in the closure type is declared const. That means that in the body of
operator()
– which is the same as the body of the lambda –
m_offset
is const. Practically speaking,
m_offset
’s type is
const int
, and if you try to modify it inside the lambda, your compilers will exchange cross words with you:

std::for_each(v.cbegin(), v.cend(),
[=](int i) { std::cout << i + offset++; });             // error!


If you really want to modify your copy of
offset
(i.e., you really want to modify
m_offset
), you’ll need to use a mutable lambda2. Such lambdas generate closure types where the
operator()
function is not const:

std::for_each(v.cbegin(), v.cend(),
[=](int i) mutable { std::cout << i + offset++; });     // okay


When you capture a variable by value (i.e., you capture a copy of the variable),
const
is effectively slapped onto the copy’s type unless you slap a
mutable
on the lambda first.

In the function
f
,
offset
is never
modified, so you might well decide to declare it const yourself. This is
laudable practice, and, as one might hope, it has no effect on the
non-mutable lambda:

void f(const std::vector<int>& v)
{
const int offset = 4;
std::for_each(v.cbegin(), v.cend(),
[=](int i) { std::cout << i + offset; });           // fine
}


Alas, it’s a different story with the mutable lambda:

void f(const std::vector<int>& v)
{
const int offset = 4;
std::for_each(v.cbegin(), v.cend(),
[=](int i) mutable { std::cout << i + offset++; }); // error!
}


“Error? Error?!,” you say? Yes, error. This code won’t compile.
That’s because when you capture a const local variable by value, the
copy of the variable in the closure type is also const: the constness of the local variable is copied to the type of the copy in the closure. The closure type for that last lambda is essentially this:

class SomeMagicNameYouNeedNotKnow {
private:
const int m_offset;
public:
SomeMagicNameYouNeedNotKnow(int p_offset): m_offset(p_offset){}
void operator()(int i) { std::cout << i + m_offset++; }          // error!
};


This makes clear why you can’t do an increment on
m_offset
. Furthermore, the fact that
m_offset

is defined to be const (i.e., is declared const at its point of
definition) means that you can’t even cast away its constness and rely
on it being modifiable, because the result of attempting to modify the
value of a object defined to be const is undefined if the constness of
that object is cast away.

One might wonder why, when deducing the type of a data member for a
closure type, the top-level constness of the source type is retained,
even though it’s ignored during type deduction for templates and
auto
s.
One might also wonder why there is no way to override this behavior,
i.e., to tell compilers to ignore top-level consts when creating data
members in closure types for captured-by-value local variables. I,
myself, have wondered these things, but I have been unable to find out
the official reasoning behind them. However, it’s been observed that if
code like this were valid,

void f(const std::vector<int>& v)
{
const int offset = 4;
std::for_each(v.cbegin(), v.cend(),
[=](int i) mutable { std::cout << i + offset++; }); // not legal, but
}                                                                     // suppose it were


a casual reader might mistakenly conclude that the const local variable
offset
was being modified, even though it would actually be the closure’s copy of
offset
that was being operated on.

An analogous concern could be the reason why
operator()

in a closure class generated from a non-mutable lambda is const: to
prevent people from thinking that the lambda is modifying a local
variable when it is actually changing the value of its copy of that
variable:

void f(const std::vector<int>& v)
{
int offset = 4;                                           // now not const
std::for_each(v.cbegin(), v.cend(),
[=](int i) { std::cout << i + offset++; }); // now not mutable, yet
}                                                             // the code is still invalid


If this code compiled, it would not modify the local variable
offset
, but it’s not difficult to imagine that somebody reading it might think otherwise.

I don’t personally find such reasoning persuasive, because the author
of the lambda has expressly requested that the resulting closure
contain a copy of
offset
, and I think we can
expect readers of this code to take note of that, but what I think isn’t
important. What’s important is the rules governing the treatment of
types when variables used in lambdas are captured by copy, and the rules
are as I’ve described them. Fortunately, the use cases for lambdas that
modify their local state are rather restricted, so the chances of your
bumping up against C++’s treatment of such types is fairly small.

Summary

In the following, the italicized terms are of my own making; there is nothing standard about them.

An object’s declared type is the type it has at its point of declaration. This is what we normally think of as its type.

The effective type of an object is the type it has when accessed via a particular expression.

In C++0x,
decltype
can be used to distinguish an
object’s declared and effective types. There is no corresponding
standard functionality in C++98, although much of it can be obtained via
compiler extensions (e.g., gcc’s
typeof
operator
) or libraries such as Boost.Typeof.

During type deduction for template parameters and C++0x
auto
variables, top-level consts and volatiles are ignored.

The declared types of copies of variables captured by value in
lambda expressions have the constness of the types of the original
variables. In non-mutable lambdas, the effective types of such copies
are always const, because the closure class’s
operator()
is a const member function.

Acknowledgments

Walter Bright and Bartosz Milewski provided useful comments on drafts
of this article, and Eric Niebler generously agreed to prepare it for
publication at C++Next.

Displaying Types

Regrettably, there’s no standard way to display the type of a variable or expression in C++. The closest thing we have is the
name
member function in the class
std::type_info
, which is what you get from
typeid
. Sometimes this works reasonably well,

int i;
std::cout << typeid(i).name();      // produces "int" under Visual C++,
// "i" under gcc.


but sometimes it doesn’t:

const int ci = 0;
std::cout << typeid(ci).name();     // same output as above, i.e.,
// const is omitted


You can improve on this by using template technology to pick a type
apart, produce string representations of its constituent parts, then
reassemble those parts into a displayable form. If you’re lucky, you can
find somebody who has already done this, as I did when I posted a query
about it to the Usenet newsgroup comp.lang.c++. Marc Glisse offered a
Printtype

template that produces pretty much what you’d expect from the following
source code when compiled under gcc. (These examples are taken from
Marc’s code.)

struct Person{};
typedef int (*fun)(double);
std::cout << Printtype<unsigned long*const*volatile*&()>().name();
std::cout << Printtype<void(*&)(int(void),Person&)>().name();
std::cout << Printtype<const Person&(...)>().name();
std::cout << Printtype<long double(volatile int*const,...)>().name();
std::cout << Printtype<int (Person::**)(double)>().name();
std::cout << Printtype<const volatile short Person::*>().name();


Marc’s approach uses variadic templates, a C++0x feature which, as
far as I know, is currently supported only under gcc, but with minor
alternation, I expect his code would work as well under any largely
conforming C++98 compiler. Information on how to get it is available via
the references below.

Further Information

C++0x entry at Wikipedia. A nice, readable, seemingly up-to-date overview of language and library features introduced by C++0x.

Working Draft, Standard for Programming Language C++,
ISO/IEC JTC1/SC22/WG21 Standardization Committee, Document N3225, 27
November 2010. This is the nearly-final C++0x draft standard. It’s not
an easy read, but you’ll find definitive descriptions of
decltype
in section 7.1.6.2 (paragraph 4), of
auto
in section 7.1.6.4, and of lambda expressions in section 5.1.2.

decltype entry at Wikipedia. A nice, readable, seemingly up-to-date overview of C++0x’s
decltype
feature.

Decltype (revision 5),
Jaako Järki et al, ISO/IEC JTC1/SC22/WG21 Standardization Committee
Document N1978, 24 April 2006. Unlike the draft C++ standard, this
document provides background and motivation for
decltype
, but the specification for
decltype
was modified after this document was written, so the specification for
decltype
in the draft standard does not exactly match what this document describes.

Referring to a Type with typeof, GCC Manual. Describes the
typeof
extension.

Boost.Typeof, Arkadiy Vertleyb and Peder Holt. Describes the Boost.Typeof library.

Deducing the type of variable from its initializer expression,
Jaako Järvi et al, ISO/IEC JTC1/SC22/WG21 Standardization Committee
Document N1984, 6 April 2006. This document gives background and
motivation for
auto
variables. The specification it
proposes may not match that in the draft standard in every respect. (I
have not compared them, but it’s common for details to be tweaked during
standardization.)

Lambda Expressions and Closures: Wording for Monomorphic Lambdas (Revision 4),
Jaako Järvi et al, ISO/IEC JTC1/SC22/WG21 Standardization Committee
Document N2550, 29 February 2008. This is a very brief summary of lambda
expressions, but it references earlier standardization documents that
provide more detailed background information. The specification for
lambdas was revised several times during standardization.

Overview of the New C++ (C++0x),
Scott Meyers, Artima Publishing, 16 August 2010 (most recent revision).
Presentation materials from my professional training course in
published form. Not as formal or comprehensive as the standardization
documents above, but much easier to understand.

String representation of a type,
Usenet newsgroup comp.lang.c++, 28 January 2011 (initial post).
Discussion of how to produce displayable representations of types.
Includes link to Marc Glisse’s solution.

Top-level volatiles are treated the same way as top-level consts, but
to avoid having to repeatedly say “top-level consts or volatiles,” I’ll
restrict my comments to consts and rely on your understanding that C++
treats volatiles the same way.

Either that or cast the constness of your copy of
offset
away each time you want to modify it. Using a mutable lambda is easier and yields more comprehensible code.

For details, refer to: http://scottmeyers.blogspot.com/2011/03/appearing-and-disappearing-articles-at.html http://cpp-next.com/archive/2011/04/appearing-and-disappearing-consts-in-c/
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: